Meeting Reuse Requirements Effectively

Visual Basic offers a rich selection of methods to achieve effective reusability. As with many things that involve a choice, you can choose either correctly or incorrectly. In a large computer application, an incorrect choice can mean weeks or months of extra effort. Mistakes will also inevitably occur because of the wide range of options available. For example, you can be sure that many Visual Basic programmers will be eager to create custom controls simply because they now can thanks to Visual Basic 5 and 6. A particular problem to watch for is the "gold-plating" syndrome, which occurs when a programmer spends too much time adding unnecessary features. Although such features work, they serve only as superfluous gloss on the application—and worse yet, the time spent fiddling with them can jeopardize your schedule.

The following sections provide an overview of the different methods you can use to achieve effective reuse.

Code Reuse Using Objects

Creating and distributing objects is one of the most powerful means of achieving reusability. An object is a discrete unit of functionality. Object components within Visual Basic can be either internal or external to an application, and they can be shared between applications, as shown in Figure 14-1. External object components have an advantage in that they can be physically deployed anywhere on a network. By strategically deploying object components to maximize resources, you can save possibly thousands of dollars when you consider that a typical development's cost comprises both development and deployment overheads.

Figure 14-1 Applications sharing object components

Inherently, object components are loosely coupled and generic. Each object component in Figure 14-1 is totally self-contained and can be used by any number of applications or components. An object component should have no "knowledge" of the outside world. For example, if you have an object component that contains a method to retrieve a list of customers for a given criterion, that method should accept the criterion as input and return the list of customers. It is up to the caller, or client, using the object's, or server's, method to display or process the results. You could code the object's method to fill a list of customers on the form, but that object would be tied to the particular interface component on the form. If you wanted to reuse the object's method in another application, you would need to have an interface component of the same type and name on that application's form. The object would therefore be tightly coupled because it "knew about" the interface.

From a business perspective, object components provide a way of controlling and managing business logic. Business logic consists of rules that can change to meet the needs of the business. By placing such logic in object components and locating these on a server, you can make changes instantly available with low installation overhead, especially since polymorphic (multiple) interfaces in Visual Basic 6 allow you different interfaces within the same component. For example, if a bank were offering an additional 2 percent interest to any customers with over $10,000 in their savings accounts, the functionality could be specified in an account calculations object, as shown in the following pseudocode:

Procedure Calculate Monthly Interest For Customer Cust_No     High_Interest_Threshold = 10000     Get Customer_Balance for Customer Cust_No     Get Interest_Rate_Percent     If Customer_Balance < High_Interest_Threshold Then         Add Interest_Rate_Percent to Customer_Balance     Else         Add Interest_Rate_Percent + 2% to Customer_Balance     End If End Procedure 

In this example, the special offer might have been an incentive that was not anticipated when the application was originally designed. Thus, implementing the functionality in a non-object-component environment would probably involve quite a few additional steps:

  • Adding a high interest threshold field to the database

  • Adding to the maintenance functionality to amend the high interest threshold

  • Amending the monthly balance calculation formula to include an additional calculation

  • Shutting down the database to make changes

  • Rebuilding the application EXE file

  • Testing and debugging

  • Reinstalling the application on the client PCs

As you can see, a relatively simple change request can involve a lot of time and money. Using an object component design, you can drastically reduce the amount of effort required to implement such a change. To make the same change in an object component system requires slightly less effort. The differences are explained here:

  • The account calculations object calculates interest payments, so locating the code module to change will be fairly simple.

  • Because only the account calculations object requires a change, only this component needs to be rebuilt. With this type of design, the object components are most likely to be installed on a server so that only one copy of the object needs to be reinstalled. The object can also be made to work in the new way for some applications and in the old way for other applications without any of the applications changing at all.

  • Testing will be limited to the object that has changed because its functionality is completely encapsulated.

This very simple example shows how objects—in this case, distributed objects—offer a major advantage in terms of maintenance. A good example of how shrewd object distribution can save money is one that Peet Morris often uses in his seminars:

If you imagine an application that utilizes a word processor to print output, by installing the print object and a copy of the word processor on the server, each user can access the single installation for printing. Whether you have 5 or 500 users, you still need only one copy of the word processor.

Another advantage of distributed objects is that you can install object components on the most suitable hardware. Imagine that you have several object components, some that perform critical batch processing and some that perform non-critical processes. You can put the critical tasks on a dedicated fault-tolerant server with restricted access and locate the non-critical processes on a general-purpose server. The idea here is that you don't necessarily need all your hardware to be high specification: you can mix and match. The capability to move object components away from the desktop PC means that the client or user interface code can be much smaller and won't require high-end PCs to run. With distributed objects, it's a simple enough task to relocate object components so that you can experiment almost on the fly to determine the best resource utilization.

Cost benefits of object reuse

So far, we've discussed how object components can be reused by many applications and how maintaining applications using this design can be simplified. The reuse advantage should not be underestimated, especially by business managers. Examine Table 14-1, which shows the budget estimate for a warehouse stock inventory and ordering system. The development is estimated to be completed within 12 months from start to finish, including the time required for the project manager and the technical lead to prepare the functional and technical specifications.

Table 14-1 Budget Estimate for a Warehouse Stock Inventory and Ordering Application

Resource Cost per Day ($) Duration (Months) Cost($)*
1 Project Manager 750 12 180,000
1 Technical Lead 600 12 144,000
3 Programmer 450 x 3 = 1,350 10  270,000
1 Tester 300 5 30,000
TOTAL 3000   624,000
 *Based on working 20 days a month

Some simple arithmetic shows that if all goes as planned, based on a five-day week, the total cost of the project will be $624,000. The company has decided that this will be the first of three development projects. The second will be a system to allow the purchasing department to do sales-trend analysis and sales predictions. The budget estimate for the second project is shown in Table 14-2.

Table 14-2 Budget Estimate for a Sales-Trend Analysis Application

Resource Cost per Day ($) Duration (Months) Cost($)*
1 Project Manager 750 10 150,000
1 Technical Lead 600 10 120,000
2 Programmer 450 x 2 = 900 8 144,000
1 Tester 300 3 18,000
TOTAL 2550   432,000
 *Based on working 20 days a month

The third project will be a Web application that allows customers to query availability and price information 24 hours a day. The budget estimate for this project is shown in Table 14-3.

Table 14-3 Budget Estimate for an Internet Browser

Resource Cost per Day ($) Duration (Months) Cost($)*
1 Project Manager 750 9 135,000
1 Technical Lead 600 9 108,000
1 Programmer 450 8 72,000
1 Tester 300 4 24,000
TOTAL 2100   339,000
 *Based on working 20 days a month

If we examine all three applications as a single system and then build the applications in sequence, it becomes apparent that the second and third applications will require far less development time than the first because they build on existing functionality. One advantage here is that building the second and third systems need not affect the first system. This situation is ideal for phased implementations. The success of this strategy depends largely on how well the design and analysis stages were completed. Figure 14-2 shows the design of all three applications. The three applications are treated as a single development for the purpose of planning. Reusable functionality is clearly visible, and although the developments will be written in phases, the reusable components can be designed to accommodate all the applications.

In the development of a multiple application system, design is of the utmost importance. It is the responsibility of the "business" to clearly define system requirements, which must also include future requirements. Defining future requirements as well as current ones helps designers to design applications that will be able to expand and change easily as the business grows. All successful businesses plan ahead. Microsoft plans its development and strategies over a 10-year period. Without knowledge of future plans, your business cannot make the most of object component reusability.

Looking back at the application design in Figure 14-2, you can see that all three systems have been included in the design. You can clearly see which components can be reused and where alterations will be required. Because the design uses object components, which as you'll recall are loosely coupled inherently, it would be possible to build this system in stages—base system 1, then 2, then 3.

Let's consider the estimates we did earlier. The main application was scheduled to be completed in 12 months and will have 12 major components. So ignoring code complexity, we can do a rough estimate, shown in Figure 14-3, of how much effort will be required to implement the other two applications. Take the figures with a grain of salt; they're just intended to provide a consistent comparison. In reality, any computer application development is influenced by all kinds of problems. It's especially important to keep in mind that new technologies will always slow down a development by an immeasurable factor.1

Figure 14-2 Single development comprises three application systems

Figure 14-3 Rough time estimate for coding and testing three applications

The estimates for the three applications when viewed as standalone developments could well be feasible. When viewed as a whole, they give a clear picture of which components can be shared and therefore need to be written only once. The reusable components can be designed from the start to meet the needs of the second and third applications. Although this might add effort to the first project, the subsequent projects will in theory be shortened. Here are the three major benefits:

  1. The design anticipates and allows for future expansion.

  2. The overall development effort is reduced.

  3. Functionality can be allocated to the most appropriate resource. For example, a print specialist can code the print engine without affecting the interface coder.

As you can see, object components provide a number of advantages. Object components are vital to high-level code reuse because of their encapsulation, which allows functionality to be allocated to the most suitable resource. As Fred Brooks points out in The Mythical Man-Month: Essays on Software Engineering (Addison-Wesley, 1995), "Only through high-level reuse can ever more complex systems be built."

Object component practicalities

Object components are built using a special Visual Basic module type called a class module. The class module can contain properties, methods, and events and can consume properties, methods, and events from other classes (described later). Our example diagrams so far have been high level; that is, only the overall functionality of the object has been shown. In reality, an object component will usually consist of contained classes—each with properties, methods, and events. Figure 14-4 shows a more detailed example of how applications might interact with object components.

For the programmer, using object components couldn't be simpler. An object component is written in ordinary Visual Basic code. To use an object, the programmer simply has to declare the object, instantiate it, and then call its methods and properties. Two additional and powerful features that greatly increase the power of object components were added to Visual Basic 5: the Implements statement and the Events capability.

The Implements statement allows you to build objects (class objects) and implement features from another class (base class). You can then handle a particular procedure in the new derived class or let the base class handle the procedure. Figure 14-5 shows an imaginary example of how Implements works. The exact coding methods are not shown here because they are covered fully in the online documentation that comes with Visual Basic 6. The example in Figure 14-5 is of an airplane autopilot system.

Figure 14-4 Classes within object components

Figure 14-5 shows a base Autopilot class that has TakeOff and BankLeft methods. Because different airplanes require different procedures to take off, the base Autopilot class cannot cater to individual take-off procedures, so instead it contains only a procedure declaration for this function. The BankLeft actions, however, are pretty much the same for all airplanes, so the Autopilot base class can perform the required procedures.

There are two types or classes of airplane in this example: a B737 and a Cessna. Both classes implement the autopilot functionality and therefore must also include procedures for the functions that are provided in the Autopilot base class. In the TakeOff procedure, both the Cessna and B737 classes have their own specific implementations. The BankLeft procedures, however, simply pass straight through to the BankLeft procedure in the Autopilot base class. Now let's say that the BankLeft procedure on the B737 changes so that B737 planes are limited to a bank angle of 25 degrees; in this case, you would simply replace the code in the B737 class BankLeft procedure so that it performs the required action.

Visual Basic 4 users might have noticed something interesting here: the Cessna and B737 classes have not instantiated the Autopilot class. This is because the instancing options for classes changed with Visual Basic 5. It is now possible to create a class that is global within the application without having to declare or instantiate it. Here are the new instancing settings:

  • PublicNotCreatable Other applications cannot create this class unless the application in which the class is contained has already created an instance.

  • GlobalSingleUse Any application using the class will get its own instance of the class. You don't need to Dim or Set a variable of the class to use it.

  • GlobalMultiUse An application using the class will have to "queue" to use the class because it has only a single instance, which is shared with any other applications using the class. You don't need to Dim or Set a variable of the class to use it.

Only classes in ActiveX EXE projects have every Instancing option available to them. ActiveX DLL projects don't allow any of the SingleUse options, and ActiveX Control projects allow only Private or PublicNotCreatable.

The Events capability in Visual Basic 5 and 6 is the second useful and powerful new feature that is available to object classes. Essentially, it allows your object class to trigger an event that can be detected by clients of the object class. The "Introducing the progress form" section gives an example of how events are used.

Figure 14-5 Example using the Implements statement

Reuse and Modules

With all the wonderful new techniques available it is easy to forget the humble standard module. Standard modules are ideal in cases where you have an internal unit of functionality that does not need to have an instance and might need to be available throughout the application. One feature of standard modules that is often forgotten, or not known, is that modules can contain properties and methods (but not events). A common problem with some applications is that the infrastructure code required to hold the application together is often written in to one or more standard modules each containing a multitude of public variables. While achieving the desired effect it can make for extremely difficult debugging and reuse. I'm sure you can all think of an instance where you were not sure if a particular public variable existed that would meet your requirements, and accordingly you created another—just to be sure!

The practice of using public variables drastically cuts down the potential for reuse because the coupling of components becomes unclear. For example, if a particular function depended on the variable nPuUserPrivilege, how would you know what area of functionality owned this variable? Sure, you could press Shift+F2 to go to the definition, but the chances are there would be many more such instances. A better way might be to make the variable a public property variable. By doing so you have the added advantage of being able to build event code, giving you the opportunity to perform certain actions whenever the property is accessed. Another benefit is that you can add a description that appears in the Object Browser with the property or method. This can be done by choosing Procedure Attributes from the Tools menu. One technique that is good practice is to give your modules meaningful names and specify the scope when accessing one of its properties. For example, you could have two public properties called IsTaskRunning in separate modules and access them individually, like this:

If modFileProcessing.IsTaskRunning = True then ... If modReportProcessing.IsTaskRunning = True then ... 

Standard modules have an advantage in that they cannot have multiple instances and they do not need to be created—they are there right when your application starts. This makes them ideal for general-control flow logic and high-level application control. The sample code below shows part of a standard module whose task is to control the user interface state. Notice in particular that rather than having to set many flags, the whole interface state can be configured by setting just one property.

Form code

 Sub Form_Load()     Set basUIControl.MainForm = Me End Sub Sub SomeProcess()     basUIControl.State = UIC_MyProcessStarted     basUIControl.UpdateProgressBar 0     For nCount = 1 To 70         ... do process ...         basUIControl.UpdateProgressBar (nCount \ 70) * 100     Next nCount          basUIControl.State = UIC_ProcessComplete End Sub 

Standard module basUIControl

 Public Enum UIC_StateConstants     UIC_MyProcessStarted     UIC_ProcessComplete     .     .     . End Enum Private Enum UIC_MenuType     DisableWhileProcessing     DisableOnUserType     .     .     . End Enum Private m_MainForm As Form Public Property Set MainForm(frmForm As Form)     Set m_MainForm = frmForm End Property Public Property Let State(nState As UIC_StateConstants)     Dim mnuMenu As Menu     Select Case nState         ' If a process is starting then disable menus that are not allowed         ' while processing.         Case UIC_MyProcessStarted             For Each mnuMenu In m_MainForm                 If mnuMenu.Tag = UIC_MenuType.DisableWhileProcessing Then                     mnuMenu.Enabled = False                 End If             Next mnuMenu         Case ... End Property

The code shown above is totally legal and shows how—with careful planning and design—you can make even "environmental" type code reusable and loosely coupled. Note that the basUIControl module stores a pointer to the application's main form. This means that we can manipulate the form without actually knowing anything about it. In this example each menu item is assumed to have a Tag value specifying what type of menu item it is. In our logic, when the state is set to process running we use the tag to selectively turn off certain menu items.

In terms of reuse we have several benefits:

  • If another UI related function is required, it is obvious where it should go.

  • If a programmer needs to use a procedure from this module, it is clear from the Object Browser exactly what each property and method is for —assuming, of course, that you remember to fill in a description.

  • It is far easier to see from the Object Browser if a particular property already exists, especially if similar logic is grouped.

From the progression of public variables to properties, one aspect that might not immediately spring to mind is that of application design. Because all our module's attributes now conform to a class specification, there is no reason why we cannot include the module in an object diagram. Normally we view public variables as elements whose scope is global, but in so doing they in effect have no scope—that is, they are not really part of any particular functional area of an application. By converting these variables to properties, we have (as a side effect) scoped these attributes to a specific functional element even though they are global. This can have enormous benefits in terms of application design because we can account for every last member. We are no longer in the position of having tightly coupled control elements. The picture below shows a simple program that has been reverse engineered using the Visual Modeler application that comes with Microsoft Visual Studio 98. Note that this sample is intended to illustrate that modules and their associated properties and methods are represented in the same way as class objects. This allows us to account for all elements in an application if we use public properties rather than public variables.

Figure 14-6 An object diagram created by the Visual Modeler application that comes with Microsoft Visual Studio 6

Forms as Reusable Components

Many developers overlook the fact that forms can make very good reusable components. In addition to the traditional code reuse benefit of reduced development time through the use of previously debugged and tested code, a second, possibly less tangible benefit is user interface consistency across applications. With the exploding office suite market, this need for a consistent user interface has become far more apparent over the last two to three years. A major selling point of all the competing suites is consistency of user interface across the separate suite applications. Consistency reduces wasted time: the user doesn't have to pore over many different manuals learning how things work. Once the user has mastered something in one application, he or she can apply the same skill to the other members of the suite. Reusing forms can be a real win-win tactic. In the long run, you save development time, and the user requires less training.

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 reusable

As 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 form

The 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 events

The 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 form

The 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 public

Although 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 

The code for the progress form and the ProgressForm control are all on the book's companion CD.

Creating Your Own Controls

A lot of interest in Visual Basic 5 and 6 has been focused on the ability to create custom controls. This ability has greatly extended the capabilities of the product, in a way that some felt should have been possible from the start.

Prior to Visual Basic 4, the custom control was the primary source of reuse. Controls and their capabilities took center stage and appeared to take on lives of their own, becoming software superstars. In some instances, complete projects were designed around a single control and its capabilities! The problem with this was that you couldn't write these wonderful, reusable controls using Visual Basic—you had to resort to a lower-level language such as C++. This situation was hardly ideal, when one of the reasons for using Visual Basic in the first place was to move away from having to get your hands dirty with low-level code.

With Visual Basic 4, the emphasis moved away from controls to classes and objects as a means of reuse. Controls are great as part of the user interface of an application, but they're not really cut out to provide anything else because of their need to be contained in a form. This limitation is significant if you want to write a DLL or a distributed object.

Although the ability to write your own controls is a major boon, it isn't the solution for all your problems. Don't overuse this ability just because you can or because you want to. You can do a great deal much more effectively than by resorting to writing a control. Again, beware of the gold-plating syndrome.

Creating the Year 2000 DateBox control

The DateBox control is an ActiveX control that provides a means of obtaining and displaying date information in a Year 2000 compliant format. This case study discusses the design goals for the control as well as including a more general discussion of ActiveX control creation in Visual Basic 6.0.

Of utmost importance with the DateBox control is the ability to have a Date property whose data type is Date. Chapter 8, which focuses on the Year 2000 problem, discusses the issues around dates being stored in data types other than the Date type, so it is a foregone conclusion that type Date will be used in the control. An interesting problem arises from using the Date type: binding to a data source whose type is Nullable Date is not possible—the control would neither be able to read nor write a Null value from the data source. In real-world applications it is quite likely that a date value might legitimately need to be Null. For example, date of birth might be optional on an application form if the applicant is over 21. To get around this problem the DateBox control has a DateVariant property that is of type Variant and can be data-bound. Depending on the developer's preference, this property can return a valid date (of type Variant, subtype Date), a Null, or an Empty. The DateVariant cannot return an invalid or noncompliant —if the user attempts to read such a date, an error is raised. The DateVariant property can be set to an illegal date and this event is treated as if a user had manually typed the value.

The second design goal is to create an interface that allows the user to enter date values in a manner that does not restrict their method of working. A user might choose to enter a date in the format "10/21/1998" or "5 mar 1998." Any valid date syntax is accepted by the control providing it conforms to either the long or short date format as defined in the Regional Settings of the Control Panel (meaning the Day, Month, and Year must be in correct order). Additionally, when a date is entered, the year must be entered using four digits. The control deems a date to be invalid if a full four-digit year is not entered.

In order to achieve unobtrusive date input, validation is performed in two stages. The first validation mechanism activates when the control's date value changes, either via user input or programmatically. The foreground and background colors are changed to "error colors" specified by the developer when the control's date is not valid. By default the colors are inverted so that time isn't spent configuring settings if the default will suffice. When a validation error occurs in this first stage the user receives no other prompt. In this way the user can continue to work unobstructed until such time as he or she feels inclined to rectify the error.

The second stage of validation is triggered when the DateY2K or DateVariant property is read. When this situation occurs, the error notification is either by message box, Visual Basic error, or a change in the control's text to a predefined message. The error action is selected at design time by the developer.

Because the control is Year 2000 compliant, it follows that the control must display dates in a compliant manner, i.e., with a four-digit year. The control's display format can be toggled between the system's long or short date format. However, no matter which format is selected, the control adjusts the format to always use a four-digit year. Note that the system's original format is not modified.

In practice it is not possible to create a property called Date because this is a Visual Basic keyword. Therefore the control's date property is actually called DateY2K.

The DateBox control is based on the intrinsic TextBox control and contains most of the properties and methods of this base control.

The Properties window does not display picklists for Integer or Long properties as it does with most other ActiveX controls. In order to have a picklist for these properties you need to declare a public enumerated type and set the control's Get and Let declarations to this type, as shown here:

Public Enum DateBox_Error_Actions     dbx_RaiseError = 0     dbx_ShowMessage     dbx_ShowText     dbx_MessageAndText End Enum Public Property Get ErrorAction() As DateBox_Error_Actions     .     .     . End Property 

Using the method above will cause the Properties window to display a picklist for the ErrorAction property and offer the following choices:

    0 - dbx_RaiseError     1 - dbx_ShowMessage     2 - dbx_ShowText     3 - dbx_MessageAndText 

A feature you can make use of here is the ability to have enumerated constant names with spaces. You achieve this by enclosing the constant name in square brackets, as in this example:

Public Enum DateBox_Error_Actions     [Raise Error] = 0     [Show Message]     [Show Text]     [Message And Text] End Enum 

The code above will appear in the picklist as

    0 - Raise Error     1 - Show Message     2 - Show Text     3 - Message And Text 

In code terms the only disadvantage of using the "pretty" display is that developers wanting to use your control will have to use the bracketed syntax or declare their own constants.

When creating a control that encapsulates an intrinsic control, note that although you can choose to subclass the properties of that control, properties such as MousePointer and DragMode will appear in the Properties window as non-picklist items. This is because their property Let and Get procedures are declared as Integer or Long. In this case you can change the property type to Visual Basic's predefined data type, for example, you can use VBRUN.MousePointerConstants as the property type for MousePointer. An interesting issue is raised here with regard to coding standards. Take for instance the MSComctlLib.BorderStyleConstants. They are defined, and will appear as shown here:

    ccFixedSingle     ccNone 

These are the values you'll see in the Properties window for, say, a ListView control. However, for a TextBox control, you'll see "1 - Fixed Single" and "0 - None." Which style should you use?

One solution to the style problem is to have two sets of public enumerations per property category—one with a pretty display and one that the developer can use. There is an added advantage to this method. Look at this example:

Public Enum My_Property_Type     None = 0     [Fixed Single] End Enum Public Enum My_Property_Type_Internal     ptMin = -1     ptNone     ptFixedSingle     ptMax End Enum 

In this case, if the developer uses the My_Property_Type_Internal constants, validation within the property Let becomes much simpler. For instance, to validate input you could code the following:

Public Property Let Aproperty(Value As My_Property_Type)     If Value =< ptMin Or Value >= ptMax Then         *** Error ****     Else         .         .         .     End If End Property 

Now if at any time in the future you add or remove an enumerated constant, no change to the validation code is necessary. Alas, you cannot use private enumerated types for your property's values, so you do have to export both enumerations.

One last point on properties: if you declare a property as an enumerated type, hoping that the Property Page Wizard will create a combo selection box for you—it won't! In fact it will not allow you to select that property for inclusion in the property page.

Property pages can be a useful feature to add to your control. A property page is essentially a separate form or collection of forms that you can create to allow the user to set design time properties of your control. You might have seen property pages when you selected Custom from the Properties window, or choose Properties from the popup menu of a control.

There are some problems with property pages. In general I would advise against using them if you have many properties in a particular category, or you do not have much development time.

The Property Page Wizard cannot handle lots of controls. That is, it does not error, but will place controls off screen if they won't all fit. If you want picklists, you have to create them yourself. Another problem is that not all property page events fire when expected (or at all for that matter), as in the case of LostFocus and GotFocus—there is no way to determine when a particular page has been selected.

Within the property page object, you obtain the control's data using the SelectedControls object. The ReadProperties event copies your control's property data into module variables; however, the Initialize event happens before the ReadProperties event. In the Initialize event you cannot access the SelectedControls object. What this means in English is that if you want to access your properties during initialization, you can't! As the GotFocus event doesn't fire either, this effectively means than you can't easily perform start up logic. If you even want to attempt this feat it will mean setting lots of flags and writing inefficient code!

Writing a property page is the same as writing a screen form. If the default pages produced by the Property Page Wizard will suffice for your purposes, go ahead and use them. If on the other hand you have special property page requirements, bear in mind that you might have to write most of the code yourself. Also remember that tasks that involve reading control properties at initialization time can prove troublesome.

In addition to the primary design goals, further goals include making the DateBox control adaptable to the end user's needs. In real-world applications it is quite likely that a date input might need to be limited to a specific range. For example, a business rule might dictate that an applicant's date of birth field be in the past, or that the applicant's age be within a certain range. The MinDate and MaxDate properties of the DateBox allow for such rules. Another likely scenario is that the business operates on five-day week and as a result certain dates—such as delivery dates—cannot be weekend dates. The control allows flexibility here by providing a series of properties:

    EnableSunday     EnableMonday     .     .     .     EnableSaturday 

Setting these properties to a Boolean value allows you to effectively exclude weekdays from the valid date range. Each weekday by default is enabled, and the DisabledDayText property lets the developer specify a message to display when a date falls on a disabled day. A point worth mentioning here is that the default error message text that is displayed when a disabled day is entered uses the day name from the locale settings. It is always a good idea to use localized values where possible;. for example, display a date using the Long Date or Short Date formats defined in the Regional Settings rather than using a hard-coded format, such as MM/DD/YYYY.

A further feature of the DateBox control is the ability to force the control to keep focus following a validation error. Imagine the scenario where a user enters an invalid date then clicks the Save button. It is no good merely displaying an error message—after the warning is acknowledged, the Save button code will continue to execute. By retaining focus the control effectively blocks any further code execution, saving the developer from having to check for valid values before the save code executes. This feature raises one other issue, though. What if the user hits the Cancel button? First of all you do not want a date validation message to appear, and second you do not want the control to retain focus, which will in effect block the cancel operation. The solution here is to provide a CancelControl property. This property can be set at run time to a command button. When the DateBox loses focus to this control no validation occurs, and focus is not retained.

The developer is given the choice of specifying the notification means when a validation error occurs. The control might raise a Visual Basic error, display a message box, display text in the control or a combination of message box and text. For the message box and text methods, developers can override the default messages by specifying their own.

As stipulated previously, it is necessary to allow "blank" dates to be entered. To give greater flexibility, the DateBlankAction property can be set to one of the following values:

  • Raise an error

  • Return Null

  • Return Empty

Selecting one of the latter two options only affects the return value of the DateVariant property. Because the DateY2K property is a Date type it obviously cannot return either of these values—in this case zero is returned by the DateY2K property.

In some cases it is sensible to have the control display the current date as a default. This requirement is catered for by the DefaultDate property, which can be set to either "Today" or "None."

The "business rules" in the DateBox control stipulate that certain dates cannot be valid even though they are legal dates—for example if a two-digit year is entered, or a disabled day's date is entered. The IsDateLegal property determines whether a date is syntactically correct regardless of whether or not it's valid. To determine if a date is actually valid the property IsDateValid can be checked.

There are two error handling schemes you might need to employ. Property pages should be treated as standalone programs. Neither the control nor its parent will interact with the property page, therefore any error in a property page should employ a standard error transaction scheme in which events are treated as event procedures where the error is discarded.

The control, like the property page, is itself an application. However, the host application can cause events to be triggered within your control. In these instances it is the responsibility of the host application to deal with any errors raised by the control. Within the control itself, use a standard error transaction system. Treat all events as event procedures and discard the error there. Errors that are caused by an invalid action or input by the host application should not be discarded at the top level. Just like errors that you specifically want to raise, these should be propagated to the host application.

What can (and can't) Visual Basic controls do?

The following list is a brief rundown of what you can do with Visual Basic 6 controls:

  • You can create controls to be used in one of two ways. You can use the well-known ActiveX implementation and create a separate OCX file and, new to Visual Basic 5 and 6, you can create a source code module, with the new CTL extension, and compile it into an application. See the next section for more information on these two methods.

  • You'll find built-in support for creating property pages, with the inclusion of the PAG module. Standard Font and Color property pages are also provided (more on these later).

  • You can create a control that combines a number of other existing controls.

  • You can create ActiveX control projects that contain more than one control in a single OCX file.

  • You can create bound controls with very little effort.

  • You can use Visual Basic to create controls for use in other languages and within World Wide Web pages.

  • With the ability to have multiple projects within a single Visual Basic session, debugging your controls is very easy. Other server projects such as ActiveX DLLs are also simple to debug.

That's the good news. Here are the limitations:

  • Controls are not multithreaded within a single instance. They are in-process servers and as such run in the same process as their client. Visual Basic 6 only gives you the ability to have multithreaded objects that have no user interface.

  • Because they run in the same process as the client, controls created using Visual Basic 6 cannot be used with 16-bit client applications.

ActiveX or in line? That is the question

When creating controls, you need to be aware of how they are to be distributed or used. Visual Basic 5 was the first version to support controls in code (as opposed to separately compiled objects). This opens the question of whether to compile your controls into traditional ActiveX controls for distribution as separate OCX files or to use them as source code objects and compile them into your application.

As with most things in life, there is no definitive answer, but there are some factors to consider when deciding.

The case for ActiveX controls Here are some advantages of using ActiveX controls, along with a couple of drawbacks with using in-line controls.

  • ActiveX controls can be used in languages and development environments other than Visual Basic, and of course they can be used in Web pages.

  • ActiveX controls are beneficial if your control is to be used widely, across many applications.

  • With ActiveX controls, bug fixes require only the OCX file to be redistributed. If the control is in line, you might have to recompile all applications that use that control.

  • Because they are included in the client as source code, in-line controls are susceptible to hacking. They are more difficult to control (no pun intended) when curious programmers are let loose on them.

The case for in-line controls Consider the following factors when thinking about using in-line controls:

  • You might have to look into licensing implications if you're distributing your ActiveX controls with a commercial application. This is obviously not an issue with in-line controls. (Licensing is covered in more detail shortly.)

  • The reduction of the number of files that you have to distribute can make ongoing maintenance and updates easier to support with in-line controls.

Your environment will largely select your deployment policy. If you're writing an application for a system that has very little control over the desktop environment, incorporating controls into your application might well be a way of avoiding support nightmares. If the system supports object-based applications and has strong control over the desktop, the benefits of creating controls as separate OCXs are persuasive.

Licensing implications

Because of their dual nature, controls present unique licensing issues in both design-time and run-time environments. Two main issues are associated with creating and distributing ActiveX DLLs. The first involves licensing your own control. Microsoft has made this deliriously easy. Just display the Properties dialog box for your ActiveX Control project, and check the Require License Key check box, at the foot of the General tab. This creates a license key that is placed in the system Registry when your ActiveX control is installed. This key enables the control to be used within the development environment and to be included in a project. When the project is distributed, however, the key is encoded in the executable and not added to the Registry of the target machine. This prevents the control from being used within the design-time environment on that machine. Visual Basic does it all for you!

The second licensing issue surrounds the use of third-party controls embedded within your own control. When you compile your control, the license keys of any constituent third-party controls are not encoded in your control. Additionally, when your control is installed on another machine, the license key for your control will be added to the Registry, but the license keys of any of these contained controls are not. So although your control might have been installed correctly, it won't work unless the controls it contains are separately licensed to work on the target machine.

If you're writing for an in-house development, licensing will be largely irrelevant. For those writing controls for a third-party product or as part of a commercial product, however, licensing is an important issue. You need to be able to protect your copyright, and fortunately you have been given the means to do so.

Storing properties using the PropertyBag object

PropertyBag is an object introduced in Visual Basic 5. This object is of use exclusively for creating controls and ActiveX documents.

The PropertyBag object is a mechanism by which any of your control's properties set within the Visual Basic Integrated Development Environment (IDE) can be stored. All controls have to store their properties somewhere. If you open a Visual Basic form file in a text editor such as Notepad, you'll see at the start of the form file a whole raft of text that you wouldn't normally see within the Visual Basic IDE. This text describes the form, its settings, and the controls and their settings contained within it. This is where PropertyBag stores the property settings of your control, with any binary information being stored in the equivalent FRX file.

This object is passed to your control during the ReadProperties and WriteProperties events. The ReadProperties event occurs immediately after a control's Initialize event, usually when its parent form is loaded within the run-time or the design-time environment. This event is an opportunity for you to retrieve all of your stored property settings and apply them. You can do this by using the ReadProperty method of the PropertyBag object. This is illustrated in the following ReadProperties event from the DateEdit example control found on the book's companion CD in the CHAP14 folder.

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)      '     ' Load property values from storage.      '     Set m_MouseIcon = PropBag.ReadProperty("MouseIcon", Nothing)     Set Font = PropBag.ReadProperty("Font", Ambient.Font)     txtDateEdit.ForeColor = PropBag.ReadProperty("ForeColor", _ _         vbWindowText)     txtDateEdit.FontName = PropBag.ReadProperty("FontName", _         "MS Sans Serif")     txtDateEdit.FontSize = PropBag.ReadProperty("FontSize", 8.25)     txtDateEdit.FontBold = PropBag.ReadProperty("FontBold", 0)     txtDateEdit.FontItalic = PropBag.ReadProperty("FontItalic", 0)           '     ' Convert any Null dates to empty strings.      '     If IsNull(m_MinDate) Then m_MinDate = ""     If IsNull(m_MaxDate) Then m_MaxDate = "" End Sub 

The ReadProperty method has two arguments: the first is the name of the property you want to read; and the second, optional, argument is the default value of that property. The ReadProperty method will search the PropertyBag object for your property. If it finds it, the value stored will be returned; otherwise, the default value you supplied will be returned. If no default value was supplied and no value was retrieved from PropertyBag, nothing will be returned and the variable or the object you were assigning the property to will remain unchanged.

Similarly, you can make your properties persistent by using the WriteProperties event. This event occurs less frequently, usually when the client form is unloaded or after a property has been changed within the IDE. Run-time property changes are obviously not stored in this way. You would not want them to be persistent.

The WriteProperty method has three arguments: the first is the name of the property you want to store; the second is the data value to be stored; and the third is optional, the default value for the property. This method will store your data value and the associated name you supply unless your data value matches the default value. If you specified a data value that matches the default value, no value is stored, but when you use ReadProperty to find this entry in PropertyBag, the default value will be returned. If you don't specify a default value in your call to WriteProperty, the data value will always be stored.

The following code is from the WriteProperties event of the DateEdit control. It illustrates the use of PropertyBag's WriteProperty method.

Private Sub UserControl_WriteProperties(PropBag As PropertyBag)      '     ' Write property values to storage.      '     Call PropBag.WriteProperty("ForeColor", txtDateEdit.ForeColor, _         vbWindowText)     Call PropBag.WriteProperty("Enabled", m_Enabled, m_def_Enabled)     Call PropBag.WriteProperty("FontName", txtDateEdit.FontName, _         "")     Call PropBag.WriteProperty("FontSize", txtDateEdit.FontSize, 0)     Call PropBag.WriteProperty("FontBold", txtDateEdit.FontBold, 0)     Call PropBag.WriteProperty("FontItalic", _         txtDateEdit.FontItalic, 0)     .     .     . End Sub 

Property pages

Visual Basic 5 introduced property pages, which are of exclusive use to controls. These are dialog boxes you can call up from within the Visual Basic IDE that display a control's properties in a friendly tabbed dialog box format. Each property page is used as a tab within the tabbed dialog box. Visual Basic controls the tabs and the OK, Cancel, and Apply buttons for you. Additionally, you are provided with ready-made Font, Picture, and Color pages to use if necessary, which you should use whenever possible for a little more code and user interface reuse. Figure 14-9 shows the Property Pages dialog box for the DateEdit control.

Visual Basic 6 allows you to create property pages for your control. It is important that you do this. If you have gone to the trouble of writing the control in the first place, you owe it to yourself and others to make the control as easy to use as possible. Designing a property page is no different from designing a form: you can drop controls directly onto it and then write your code behind the events as usual.

When any changes are made to a property using your property page, you need to set the property page's Changed property to True. This tells Visual Basic to enable the Apply command button and also tells it to raise a new event, ApplyChanges, in response to the user clicking the OK or the Apply command button. Apply the new property values when the user clicks OK or Apply; don't apply any changes as the user makes them because by doing so, you would prevent the user from canceling any changes: the ApplyChanges event is not raised when the Cancel command button is clicked.

Since more than one control can be selected within the IDE, property pages use a collection, SelectedControls, to work with them. You'll have to consider how each of the properties displayed will be updated if multiple controls are selected. You wouldn't want to try to set all of the indexes in an array of controls to the same value. You can use another new event, SelectionChanged, which is raised when the property pages are first loaded and if the selection of controls is changed while the property pages are displayed. You should use this event to check the number of members of the SelectedControls collection. If this number is greater than 1, you need to prevent the user from amending those properties that would not benefit from having all controls set to the same value, by disabling their related controls on the property pages.

Figure 14-9 Property pages in use within the Visual Basic IDE

Binding a control

As mentioned previously, Microsoft has also given us the ability to bind our controls (through a Data control or a RemoteData control) to a data source. This is remarkably easy to do as long as you know where to look for the option. You have to select Procedure Attributes from the Tools menu. This will display the Procedure Attributes dialog box shown in Figure 14-10.

This dialog box is useful when you're designing controls. It allows you to select the Default property and the category in which to show each property within the Categorized tab of the Properties window. It also allows you to specify a property as data-bound, which is what we're interested in here. By checking the option Property Is Data Bound in the Data Binding section, you're able to select the other options that will define your control's bound behavior.

Option Meaning
This Property Binds To DataField This option is fairly obvious. It allows you to have the current field bound to a Data control. Visual Basic will add and look after the DataSource and DataField properties of your control.
Show In DataBindings Collection At Design Time The DataBindings collection is used when a control can be bound to more than one field. An obvious example would be a Grid control, which could possibly bind to every field available from a Data control.
Property Will Call CanPropertyChange Before Changing If you always call CanPropertyChange (see below), you should check this box to let Visual Basic know.

By using the first option, you're able to create a standard bound control that you'll be able to attach immediately to a Data control and use. The remaining options are less obvious.

The DataBindings collection is a mechanism for binding a control to more than one field. This obviously has a use where you create a control as a group of existing controls, for example, to display names stored in separate fields. By selecting Title, Forename, and Surname properties to appear in the DataBindings collection, you're able to bind each of these to the matching field made available by the Data control.

You should call the CanPropertyChange function whenever you attempt to change the value of a bound property. This function is designed to check that you are able to update the field that the property is bound to, returning True if this is the case. Visual Basic Help states that currently this function always returns True and if you try to update a field that is read-only no error is raised. You'd certainly be wise to call this function anyway, ready for when Microsoft decides to switch it on.

Figure 14-10 The Procedure Attributes dialog box showing Advanced options

The wizards

Microsoft supplies two useful wizards with Visual Basic 5 and 6 that can make creating controls much easier. The ActiveX Control Interface Wizard, shown in Figure 14-11, helps in the creation of a control's interface and can also insert code for common properties such as Font and BackColor. The Property Page Wizard does a similar job for the creation of property pages to accompany your control. Once again, standard properties such as Font and Color can be selected from the ready-made property pages. Using these wizards can prove invaluable in creating the controls and their property pages and also in learning the finer points in their design.

You should use both wizards: between them, they promote a consistency of design to both the properties of your controls and the user interface used to modify these properties. The example DateEdit control used throughout this section was created using both of these wizards. Any chapter about code reuse would be churlish if it failed to promote these wizards. Of course, no wizards yet created can control what you do with the user interface of the controls themselves!

Figure 14-11 The ActiveX Control Interface Wizard

Controls: A conclusion

The ability to create controls is an important addition to Visual Basic's abilities. Microsoft has put a lot of work into this feature. As a means to code reuse, the abilities of controls are obviously limited to projects that contain forms, but the strength of controls has always been in the user interface.

A lot more could be written about controls—far more than we have space for in this chapter. Do take the time to read the Visual Basic manuals, which go into more depth, and experiment with the samples. After all, writing controls in Visual Basic is certainly much easier than writing them in C++!

Using a ROOS

Another aid to reusability, first mentioned in Chapter 1, is the ROOS (Resource Only Object Server), pronounced "ruse." We've referred to object servers as object components for most of this chapter, but these are two different names for the same object. (To be politically correct, they should really be called ActiveX components, but ROAC is not as easy to pronounce!) A ROOS essentially stores string, bitmap, and other resources that are liable to be changed at some time. Another use for a ROOS is to store multilanguage strings. If you wrote an application to be sold in the United States and in France, using the normal Visual Basic method of setting display text in code would mean that you would have to create two versions of the application: one with English display text and captions and one with French. Obviously, this would create a significant maintenance overhead, because if you have to apply a bug fix to one of the versions, you also need to apply the change to the other. The ROOS is exactly the same in principle as a resource-only DLL as used by many C and C++ programmers. The difference between a ROOS and its DLL counterpart is that the ROOS is an object component and as such can be deployed anywhere on a network and used by any client components.

You can store many types of resources in a ROOS:

Accelerator table Group cursor
Bitmap resource Group icon
Cursor resource Icon resource
Dialog box Menu resource
Font directory resource String resource
Font Resource User-defined resource

A ROOS has two components. The first is the resource module, a special file created with an application such as Microsoft Visual C++. The resource module contains all the resources you want to store and retrieve. The second element of the ROOS is a method to retrieve a resource from the resource module. At TMS, we prefer to expand the functionality of the ROOS methods so that string values can be parsed with input parameters. The following example illustrates this.

Resource Entries

String ID:    400 String value: "The operation completed with % errors" 

Client Code

StringID = 400 MyText = GetStringFromROOS(StringID, "no") 

ROOS Code

Public Function GetStringFromROOS(StringID As String, _     Param) As String     Dim sText  As String     sText = GetString(StringID)     sText = MergeString(sText, Param)     GetStringFromROOS = sText End Function 

Result

MyText: "The operation completed with no errors" 

Many projects store custom error messages or other display text in a database file. In an error handler, the custom error text is better in a ROOS because the execution speed is much faster, and many organizations restrict access to make database changes to the database administrator—no good if you're a programmer and have to wait two days to change the caption on a button! Another excellent use of a ROOS is to store icons and bitmaps. Imagine you're lucky enough to have an artist to create all your graphics. You can create a ROOS with dummy resources, and then the artist can add the real graphics to the ROOS as they become available without having to access any source code. (No more multiple access problems!)

Creating a resource module is easy if you have the right tools. You simply enter the resources you want. Each resource has an ID value, which is a long integer. To retrieve the resource from the resource module, you simply use the LoadResData, LoadResPicture, or LoadResString command specifying the resource's ID. Figure 14-12 shows a typical resource file in Microsoft Visual C++ 6. Once the resource module is created (it's actually an RC file), you simply compile it with the RC.EXE program (supplied on the Visual Basic CD-ROM) to create a RES file that you can add to your ROOS project. You can have only one RES file in a single Visual Basic project, but one is plenty! (If you don't have access to Visual C++ or any other tool for creating resource files, you can use an editor such as Notepad. Before attempting this, however, you should study an RC file and a RESOURCE.H file to become familiar with the format.)

Obviously, any client requesting data from the ROOS will need to know the ID value for each resource. In Visual Basic 4, you would need to include your ID constants in each client application, either as hard-coded constants or in a shared type library. With Visual Basic 5 and 6, you can declare all your IDs within the ROOS as enumerated constants, which makes them automatically available to client applications.

Listing 14-1 shows a slightly more advanced ROOS that retrieves string and bitmap resources. The ROOS allows you to merge an unlimited number of tokens into a string resource. To create a string resource with tokens, simply insert a % symbol in the string where the supplied parameter(s) will be substituted.

Figure 14-12 A resource file created in Microsoft Visual C++ 6

Listing 14-1 ROOS for retrieving string and bitmap resources

' The following Enums declare the resource ID of the bitmaps ' in our RES file. The include file "resource.h" generated ' by the resource editor defines the constants to match each ' bitmap. Checking this file shows the first bitmap resource ' ID to be 101; therefore, these Enums are declared to match ' this. Public Enum BITMAPS ' *** ' *** NOTE: Any new bitmaps added must be inserted between ' *** IDB_TOPVALUE and IDB_LASTVALUE because these constants are ' *** used to validate input parameters. ' ***     idb_topvalue = 100     IDB_SELECTSOURCE     IDB_SELECTDESTIN     IDB_NUMBERSOURCE     IDB_COMPLETED     idb_lastvalue End Enum Public Enum STRINGS          ' VBP project file key ID words     IDS_VBP_KEY_FORM = 500     IDS_VBP_KEY_CLASS     IDS_VBP_KEY_MODULE     IDS_VBP_SEP_FORM     IDS_VBP_SEP_CLASS     IDS_VBP_SEP_MODULE     IDS_VBP_SEP_RESFILE     IDS_VBP_KEY_RESOURCE16     IDS_VBP_KEY_RESOURCE32          ' Procedure keywords     IDS_PROCKEY_SUB1 = 600     IDS_PROCKEY_SUB2     IDS_PROCKEY_SUB3     IDS_PROCKEY_FUNC1     IDS_PROCKEY_FUNC2     IDS_PROCKEY_FUNC3     IDS_PROCKEY_PROP1     IDS_PROCKEY_PROP2     IDS_PROCKEY_PROP3     IDS_PROCKEY_END1     IDS_PROCKEY_END2     IDS_PROCKEY_END3     IDS_PROCKEY_SELECT     IDS_PROCKEY_CASE     IDS_PROCKEY_COMMENT          ' File filter strings     IDS_FILTER_FRX = 700     IDS_FILTER_PROJECT     IDS_FILTER_CLASS     IDS_FILTER_FORM     IDS_FILTER_MODULE     IDS_FILTER_CONFIG     IDS_FILE_TEMP          ' Displayed caption strings     IDS_CAP_STEP1 = 800     IDS_CAP_STEP2     IDS_CAP_STEP3     IDS_CAP_STEP4     IDS_CAP_NUMBER IDS_CAP_UNNUMBER     IDS_CAP_CANCEL     IDS_CAP_FINISH     IDS_CAP_CANCEL_ALLOWED          ' Message strings     IDS_MSG_NOT_TEMPLATE = 900     IDS_MSG_COMPLETE_STATUS     IDS_MSG_TEMPL_CORRUPT     IDS_MSG_INVALID_CONFIG     IDS_MSG_CREATE_TMPL_ERR     IDS_MSG_NO_SOURCE     IDS_MSG_INVALID_DESTIN     IDS_MSG_SAME_SRC_DESTIN     IDS_MSG_QUERY_EXIT     IDS_MSG_ABORTED          ' Err.Description strings     IDS_ERR_GDI = 1000     IDS_ERR_PROCESS_ERROR End Enum ' Resource ROOS error constants Public Enum RR_Errors     RR_INVALID_BITMAP_ID = 2000 ' Invalid bitmap resource ID     RR_INVALID_STRING_ID        ' Invalid string resource ID End Enum Public Sub PuGetBmp(ByVal ilBitmapID As Long, _     ByVal ictl As Control)     ' Check that the ID value passed is valid. This is an     ' Assert type of message, but the class cannot be part     ' of the design environment, so raise an error instead.     If ilBitmapID <= idb_topvalue Or _         ilBitmapID >= idb_lastvalue Then                  Err.Description = "An invalid bitmap ID value '" & _             ilBitmapID & "' was passed."         Err.Number = RR_INVALID_BITMAP_ID         Err.Raise Err.Number         Exit Sub     End If      ' Load the bitmap into the picture of the control passed.     ictl.Picture = LoadResPicture(ilBitmapID, vbResBitmap) End Sub Public Function sPuGetStr(ByVal ilStringID As Long, _     Optional ByVal ivArgs As Variant) As String     Dim nIndex          As Integer     Dim nPointer        As Integer     Dim nTokenCount     As Integer     Dim sResString      As String     Dim vTempArg        As Variant     Const ARG_TOKEN     As String = "%"     sResString = LoadResString(ilStringID)     If IsMissing(ivArgs) Then GoTo END_GETRESOURCESTRING     If (VarType(ivArgs) And vbArray) <> vbArray Then         ' Single argument passed. Store the value so that we can         ' convert ivArgs to an array with this single         ' value.         vTempArg = ivArgs         ivArgs = Empty         ReDim ivArgs(0)         ivArgs(0) = vTempArg     End If     nTokenCount = 0     Do While nTokenCount < UBound(ivArgs) _         = LBound(ivArgs) + 1              nPointer = InStr(sResString, ARG_TOKEN)         If nPointer = 0 Then             ' There are more arguments than tokens in the RES             ' string, so exit the loop.             Exit Do         End If         Call sPiReplaceToken(sResString, ARG_TOKEN, _             ivArgs(LBound(ivArgs) + nTokenCount))         nTokenCount = nTokenCount + 1     Loop END_GETRESOURCESTRING:     sPuGetStr = sResString End Function Private Function sPiReplaceToken(ByRef iosTokenStr As String, _     ByVal isToken As String, ByVal ivArgs As Variant)     Dim nPointer As Integer     nPointer = InStr(iosTokenStr, isToken)     If nPointer <> 0 Then         iosTokenStr = Left$(iosTokenStr, nPointer - 1) & _             ivArgs & Mid$(iosTokenStr, nPointer + 1)     End If End Function 


Ltd Mandelbrot Set International Advanced Microsoft Visual Basics 6. 0
Advanced Microsoft Visual Basic (Mps)
ISBN: 1572318937
EAN: 2147483647
Year: 1997
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net