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:
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 questionWhen 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 implicationsBecause 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 objectPropertyBag 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.
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 pagesVisual 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 controlAs 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.
|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 wizardsMicrosoft 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 conclusionThe 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++!