The following sections provide an overview of the different methods you can use to achieve effective reuse.
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:
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:
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.
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:
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."
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:
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
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:
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
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.
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.
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
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.
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.
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.
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.
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:
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.
That's the good news. Here are the limitations:
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.
The case for in-line controls Consider the following factors when thinking about using 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.
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.
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
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
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
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
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++!
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 |