Visual Basic 5.0 Relieves
the Pain and Discomfort of
ActiveX Control Creation
Guy Eddon and Henry Eddon
|This article assumes you're familiar with Visual Basic and the Internet.|
Code for this article: VB5P1.exe (16KB)
Guy and Henry are working on a book entitled Active Visual Basic 5 to be published by Microsoft Press. Both can be reached at firstname.lastname@example.org.|
Yes, it's true, the Visual Basic® 5.0 programming systems does have a native code compiler. But the really interesting feature of Visual Basic is the support for ActiveX programming. In this article, we will look at how Visual Basic 5.0 has improved, then we'll focus on using it to create two unique types of ActiveX components: controls and documents.
BASIC has a long and venerable history as the first product Microsoft® ever produced. It became visual when it arrived on the Windows® platform. Successive versions added support for database applications (3.0) and classes (4.0) to the language. With version 5.0, Visual Basic continues its steady evolution
into an object-oriented development environment based on componentsor more precisely on COM. In addition, the Visual Basic Control Creation Edition is a pared-down p-code generating version of Visual Basic 5.0 that can only be used to create ActiveX controls. It's available from http://www.microsoft.com/vbasic.
Until now, Visual Basic was often seen as a "glue language," suitable to instantiate objects developed by other development tools. Now Visual Basic has become a language able to produce ActiveX components on its own. Let's begin by examining several of the new Visual Basic 5.0 features that relate to ActiveX technologies.
At the top of the list is the ability to create ActiveX components. Visual Basic 4.0 enabled developers to create OLE automation servers. In Visual Basic 5.0, you call them ActiveX code components. What's the difference? Nothing really, but the change in nomenclature reflects the fact that everything you build in Visual Basic is a component. Previously, if you built an application that called an OLE automation server to do some work, and then that server tried to shirk its responsibilities and employ another server to do part of its work, things could get pretty confusing. What should you call the server-turned-client? Once you make the metaphysical transition to components, everything becomes streamlined. The application is calling a component, which in turn calls another component.
Visual Basic 5.0 allows you to build components that are also connectable objects and therefore have the ability to communicate with their containerin other words, the ability to "fire events." These components are known as ActiveX controls. ActiveX controls built in Visual Basic can be used by programmers developing software in Visual Basic, Visual C++®, or in HTML documents. Another type of component you can create using Visual Basic is ActiveX documents. These components can be opened inside of Microsoft Office Binder or Microsoft Internet Explorer just like Microsoft Word documents.
Good news for those of you who wanted to call a Windows API procedure that requires a callback address but couldn't because Visual Basic didn't support pointers. Visual Basic still doesn't support pointers, but an exception is made for callback functions. The new AddressOf keyword enables you to pass the address of a Visual Basic routine to a DLL-based function. For more details see the sidebar: "The New AddressOf Keyword".
Native Code Compiler
The inability to compile programs to native code has been one of the most maligned deficiencies of Visual Basic. Previous versions of Visual Basic compiled applications into p-code (packed code, sometimes called pseudo-code), which meant that an interpreter was needed at runtime, slowing execution. Now Visual Basic has a code generator that can even optimize instructions to favor the Pentium Pro. Code generated with this option will still run on earlier processors, but less efficiently. When you build your executable in Visual Basic you can now choose native code or p-code. Note that whether you use native code or p-code in your applications, all the Visual Basic runtime files must be shipped with your application.
To test the differences between the execution times of p-code and native code, I created a sample program that computes prime numbers. While the algorithm used in IsPrime is quite elementary, it is sufficient for comparison (see Figure 1). To put the results in perspective, the sample was ported to C (see Figure 2) and timed. Figure 3 shows the results on an 90MHz Pentium computer when computing prime numbers between 1 and 50,000. As you can see, native code was two and a half times faster than p-code. The C version of the test program was only marginally faster than the native code produced by Visual Basic.
Object-Oriented Programming in Visual Basic
Is Visual Basic an object-oriented programming language? Before I stand on the soapbox and issue a pronouncement, let's review what it means to be an object-oriented programming language and then you decide for yourself. Object-oriented programming is built on the fundamental pillars of encapsulation, inheritance, and polymorphism.
Encapsulation means designing and writing your program in terms of objects that contain both code (procedures) and data (property) members. This is considered a good design since data and the code that operates upon that data are stored and used together logically. Visual Basic allows you to create class modules that can contain both code and data and therefore meets this requirement. Properly done, encapsulation imposes a slight performance penalty on software due to the extra layer of a public interface. For efficiency, it is sometimes necessary to allow certain users of an object to bypass this public interface and access your private implementation directly. In these cases, it is appropriate to specify these special users as friends, thereby giving them access to your private methods.
In a language that supports friends, other routines in a project can get access to the friendly function. Both C++ and Java support the friend concept, although the Visual Basic version is closer to Java than to C++. Technically, a friend modifies the definition of a procedure in a class module to make the procedure callable from modules that are outside the class, but are part of the project within which the class is defined. Let's say you are the creator of a class module that has a public interface and a private implementation. Now when your class is used via ActiveX automation interfaces by another application, you want the object's users to have access to the public interface only. But if your object is used from within another routine in the same project, it would be OK for the user to have access to some method declared as private. In those cases you may make that method a friend. Here is an example of a friendly function:
Friend Function Sum(X As Integer, Y As Integer)
Sum = X + Y
Inheritance refers to the ability to create a new object based on another object, copying its code and data members. Inheritance is best used when an obvious "is a" relationship can be recognized between objects. You might have a basic Tree class from which a DeciduousTree class is derived. Then you inherit Oak and Maple classes from the DeciduousTree class. Inheritance would allow your code to express the fact that an Oak "is a" DeciduousTree. Visual Basic does not currently support this type of inheritance.
Aggregation, sometimes known as containment, can be used in Visual Basic to simulate, to a certain extent, a type of inheritance. Aggregation simply means building a new object by including existing objects within. This is more easily understood by way of an example. Say you have defined a door class and now want to define a car class. Most sedans have four doors, so you simply put an array of four door objects within your car class. Thus your car contains four doors. This is not a true example of inheritance because no "is a" relationship is present. It is correct to say that a car contains doors, but not that a car "is a" door.
Polymorphism means that many classes can provide the same property or method, and a caller doesn't have to know what class an object belongs to before calling the property or method. Say you create a basic Vehicle class and implement a Go method. Then you create Airplane and Car classes, both of which inherit from the Vehicle class. In each of these subclasses you override the Go method so that the Go method in the Car class causes the car to drive, and the Go method in the Airplane class causes the plane to fly. Now, polymorphism says that if you call the Go method on a Vehicle object that actually happens to be an Airplane, then the Airplane object's Go method is called, which causes the plane to fly, whereas if it was a Car object, its Go method would cause it to drive. The clear benefit of polymorphism is the ability to write relatively general code that can then work correctly with specialized types of objects.
Most object-oriented languages provide support for polymorphism through inheritance. The Car and Airplane classes were derived from the Vehicle class. Since Visual Basic does not support implementation inheritance (the inheriting of code), you might assume that polymorphism cannot be supported. This is a common misunderstanding, since polymorphism is so often implemented as a by-product of inheritance. Visual Basic 5.0 solves this dilemma in the same fashion as COM. Inheritance is not supported in COM because the purpose of COM is to integrate objects. Polymorphism, however, lies at the heart of how COM works. You define an interface, and then any object can choose to implement it.
In Visual Basic 5.0, polymorphism is supported via interface inheritance. An interface in Visual Basic is a set of related properties, methods, and events. This interface inheritance is accomplished with the Implements keyword. Let's put Implements through its paces to see what it is really made of. In the following example, we have created three class modules: Vehicle, Car, and Plane.
Public Function Go() As String
Private Function Vehicle_Go() As String
Vehicle_Go = "vrooom"
Private Function Vehicle_Go() As String
Vehicle_Go "fly away"
Then we put together a simple form in Visual Basic with a command button. When the button is clicked, the following code is executed to test the classes.
Private Sub Command1_Click()
Dim MyVehicle As New Vehicle
Dim MyCar As New Car
Dim MyPlane As New Plane
Dim AnyVehicle As Vehicle 'Only a reference!
Dim MyVehicles As New Collection
For Each AnyVehicle In MyVehicles
Print TypeName(AnyVehicle) & " goes " & AnyVehicle.Go
When run, the program's results should look something like this:
Car goes vrooom
Plane goes fly away
The Implements keyword can also be used to implement interfaces defined in type libraries. This enables you to implement any COM interface you wish. Is Visual Basic an object-oriented programming language? Visual Basic currently supports two of the three basic requirements of a true object-oriented language. So while purists might not consider it an object-oriented programming language, Visual Basic continues to evolve in that direction.
Creating ActiveX Controls
ActiveX controls are supported by a wide assortment of development tools, including Visual C++, Visual J++, Visual Basic, Visual FoxPro, Borland Delphi, Power Builder, and Microsoft Access. Until recently, only Visual C++ allowed developers to create ActiveX controls. Now Visual Basic 5.0 has added ActiveX control creation to its bag of tricks. Using Visual Basic you can create controls for consumption in your own software projects, or for distribution and sale to other developers for use in their software. In fact, the ActiveX controls you create in Visual Basic can be used by a developer writing an application in Visual C++.
ActiveX controls built in Visual Basic (or any language for that matter) are useful not only in development tools, but also in end-user applications. For example, an ActiveX control created in Visual Basic can be embedded in an HTML document and viewed in a Web browser.
Controls are definitely one of the most complex projects that can be developed in Visual Basic. Controls are very different creatures from other types of software built with Visual Basic. Think of a standard EXE project: you develop the program in design mode and then test the program in run mode. Once you have it working, you create an EXE, which always executes in run mode. Controls, likewise, are developed in design mode and tested in run mode. Since controls never run as standalone applications, you always need a test container in which to execute the control. What's different is that once you have developed a control, produced an OCX file, and given it to another developer to use in their software, the control is back to working in Visual Basic's design mode! Even when Visual Basic is in design mode, controls are running.
Types of ActiveX Controls
Visual Basic defines three basic models for control creation. You can write your own control from scratch, enhance an existing control, or assemble a new control from several existing controls.
The first model, writing a control from scratch affords you the greatest flexibility. You can do anything you want with your control's appearance and interface simply by putting code into the Paint event procedure used to draw your control. This is the model to select when creating a new visual widget with a look unlike the standard controls.
The second model, enhancing an existing control, gives you complete freedom in adding your own properties, methods, and events. The control's properties will only be exposed if you decide to expose them. Modifying the appearance of an existing control is more difficult, however, because the control you are enhancing already contains code to paint itself. Experienced programmers can use the AddressOf operator to subclass the constituent control so that it will notify you of its Paint events, but this is not easy. These hybrid controls are rather similar to what is done in the object-oriented concept of aggregation. Recall that aggregation occurs when you build a new object by accumulating already existing objects.
The third model expounded by Visual Basic is assembling a control from several existing controls. The second and third models are somewhat similar since they both utilize existing controls to build new controls.
The UserControl Object
An ActiveX control created in Visual Basic is always composed of a UserControl object plus any additional controlscalled constituent controlsthat you choose to place on the UserControl. This is tantamount to saying that a standard Visual Basic application is always composed of a Form object. Like a Visual Basic form, UserControl objects have a code module and a visual designer. Constituent controls are used only if you wish to enhance an existing control or to create a hybrid control consisting of several existing controls. You place constituent controls on the UserControl designer and set their properties in the same manner as you place controls on a form.
If you have worked with Visual Basic, you know that there are three basic members you use when working with controls: properties, methods, and events. This combination can be said to compose the interface of an ActiveX control. The UserControl object has many standard properties, methods, and events. For example, the UserControl_Click event is fired whenever the user clicks on a control, just as the Form_Click event is fired when the user clicks on a form. Some items are unique to UserControls (see Figure 4, Figure 5, and Figure 6).
Life and Times of a UserControl
While the UserControl designer is open, an ActiveX control is actually in design mode. As soon as the UserControl designer is closed, the control enters run mode automatically. The appearance of the control's icon in the Toolbox becoming enabled is the only indication that the control has entered a running state.
The life of an ordinary form is punctuated by certain
key events, such as Initialize, Load, QueryUnload, and Unload. To create well-behaved software, it is important to know at what stage in the form's life these events are
fired. The same is true for controls. The key UserControl events are Initialize, InitProperties, ReadProperties, WriteProperties, and Terminate. Notice that controls are missing Load and Unload events. These events are superseded by the ReadProperties and WriteProperties events, respectively. Load and Unload events don't really make sense for controls, since they aren't really loaded and unloaded like forms.
Now let's turn our attention to properties. ActiveX controls have three basic categories of control properties: ambient, extender, and custom. Ambient properties give your control information about the state of its container. Extender properties are those that seem to be part of your control but are actually provided at runtime by the container. Custom properties are ones that you implement entirely on your own.
It may initially seem surprising that there is such a variety in types of properties. Many developers who write applications in Visual Basic think of the properties appearing in the Properties window as belonging to a control. In fact, some properties are not displayed in the Properties window, and of the properties displayed, some don't belong to the control. Consider the Top and Left properties of a control. They describe where the control is situated within the container. These types of properties belong to the container rather than the control, even though they appear in the Properties window.
Ambient properties are provided by the container. In fact, ambient properties are exposed by the client site in which the control is embedded, and they reflect values that the container wants to communicate to the control so that it will take on the characteristics of the form in which it lives. These are effectively hints about how the control can best display itself in the container. Think of a reptile that camouflages itself by changing color to match its surroundings; the purpose of ambient properties is to help your control camouflage the fact that it is not really a native part of its container.
For example, the ambient property BackColor informs
a control of the background color of its container. This could then be used to set the control's own background color to blend better with the container. Ambient properties don't strictly need to be obeyed, but it is polite to do so when
it makes sense. Visual Basic makes ambient properties available to your control through an AmbientProperties object, a reference to which is available in the Ambient property of the UserControl object. This leads to the syntax UserControl.Ambient.AmbientPropertyName for accessing ambient properties. It is important to realize that ambient properties are properties of the UserControl object and are therefore available only to the control. They will not appear in the finished control's Property window list, nor are they available to applications using the control.
Since ambient properties are provided by the container, some containers may not implement all the ambient properties. For example, if your ActiveX control is used in an application created in Borland Delphi, it is possible that not all ambient properties will be available to your control. To circumvent this problem, the AmbientProperties object implemented by Visual Basic contains all of the standard ambient properties defined by the ActiveX controls specification, regardless of whether they are actually provided by the container. If a particular ambient property is not implemented by a container, the AmbientProperties object simply returns a default value. This means you don't have to worry about getting error messages when using an ambient property that is not available.
By the same token, some containers may implement additional ambient properties. These properties will not be visible in the Object Browser because they are not in the Visual Basic type library; they will be exposed nonetheless by the AmbientProperties object. You can learn about such properties in the documentation for a container, and then access them as properties of the AmbientProperties object. Since these additional properties are not in the type library, Visual Basic cannot verify their existence at compile time. Therefore you should use error handling with these properties if it is likely that your control will be used in a container where they're not available. As a consequence of this lack of type library information, calls to container-specific ambient properties are always late-bound. By contrast, calls to standard ambient properties are early-bound.
The standard ambient properties are BackColor, DisplayAsDefault, DisplayName, Font, ForeColor, LocaleID, MessageReflect, Palette, ScaleUnits, ShowGrabHandles, ShowHatching, SupportsMnemonics, TextAlign, UIDead, and UserMode. In Visual Basic, you can safely ignore many of these ambient properties since there is default functionality for responding to them. The more important ambient properties are listed in Figure 7.
Assuming you want to follow the recommendations provided by the ambient properties, what should your control do if one of those ambient properties changes at runtime? Let's say you set your control's BackColor property based on the ambient BackColor property so that your control blends with its container. Eventually, during execution of the application, the background color of the container is changed. This causes your control to stick out like a chicken without feathers. To avoid this sort of embarrassing situation, you can write code that responds to the UserControl_
AmbientChanged event procedure. The AmbientChanged event is fired whenever the value of one of the ambient properties changes, allowing you to respond to this situation gracefully. The event procedure is declared as Private Sub UserControl_AmbientChanged(PropertyName As String). The PropertyName argument gives your code the name of the ambient property whose value changed. This will save you from having to reset all the ambient properties' values used in your control.
The Extender Object
Another class of properties known as extender properties would certainly be regarded by an average user as being associated with the control but are actually implemented by the container. For example, a control's size and position, its order in the tab sequence, and the Tag property fall into the category of extender properties. These properties are associated with something called the extender object, which is an object implemented by the container, generally by aggregating with the control. When the user gets or sets a property or invokes a method on the control, the extender object gets access first. If the extender object recognizes the property or method as belonging to it, it performs the actions required; if the extender object doesn't recognize it, it is passed on to the control itself.
A common example of this is the Enabled property. The typical visual control will implement this property to reflect the current state of the control. A control, however, only knows its internal state. The control might think it is enabled, but if the control is part of a form that is currently disabled, then so is the control. Those readers familiar with creating ActiveX controls using Visual C++ and the Control Developer's Kit (CDK) are already familiar with extender properties. In the CDK they are called extended properties.
The extender object of the UserControl gives you, the control designer, access to extender properties from within your control. The extender object provided by the Visual Basic runtime system provides a rich set of properties, methods, and events listed in the documentation. Again, be aware that many containers provide only a limited subset of these. The ActiveX control specification says all containers should provide Name, Visible, Parent, Cancel, and Default extender properties. Although it is highly recommended that containers implement these properties, they do not have to. Thus, you should always use error trapping when referring to the properties of the extender object in your code.
Adding Custom Properties
Custom properties are the most interesting type to talk about, primarily because it is you who implements them. Custom properties may be named anything you wish and may accept any parameters you desire. Controls rarely become useful until you add some custom properties and methods. Just as in Visual Basic class modules, the simplest type of property to implement is the kind created from a public variable with a line of code like this:
Public PropName As String
Simple properties declared with the statement
are not a good idea for controls. Control properties should always be implemented with property procedures instead of public data members; otherwise your control will not work correctly in Visual Basic. This is because you must notify Visual Basic whenever a property value changes. This is done by invoking the PropertyChanged method of the UserControl object, as shown in the following code fragment:
Private m_MyProperty As Boolean
Public Property Get MyProperty() As Boolean
MyProperty = m_MyProperty
Public Property Let MyProperty(ByVal NewValue As
m_MyProperty = NewValue
Without calling the PropertyChanged method, Visual Basic does not know that the property has been changed and needs to be saved. Since property values may be displayed in more than one place, the development environment must be notified when a property value changes so that it can synchronize the values. For example, you might have a property in both a property page and the Properties window that need to be synchronized.
If you create a program that uses an ActiveX control, and you set up the properties of that control to be just right for your application, it'll be a little disconcerting when you restart the program the next day and find that all the property values you entered have evaporated. That's why you need property persistence.
Three ActiveX control events relate to property persistence: InitProperties, ReadProperties, and WriteProperties. The InitProperties event procedure is called when a control is first embedded in a form. This gives you a chance to store default values in your custom properties. The ReadProperties event is fired whenever Visual Basic needs to retrieve the value of a custom property. This happens when the project is opened and run, for example. When the UserControl object receives its ReadProperties event, it is said to have been sited. Siting is simply the process of loading a control and placing it in the container. The WriteProperties event is fired when a custom property needs to be saved. For example, saving your project always causes the WriteProperties event procedure to be called. Here are the declarations for the three property events:
Private Sub UserControl_InitProperties()
Private Sub UserControl_ReadProperties(PropBag As
Private Sub UserControl_WriteProperties(PropBag As
Visual Basic programs use the PropertyBag object to implement property persistence. A PropertyBag is just what its name implies, a "bag" in which property values are saved. You can't see into it, and you have no idea where or how the data is saved. All you can do is put values in and take them out.
The PropertyBag class defines two methods, ReadProperty and WriteProperty.
ReadProperty(Name As String, [DefaultValue])
WriteProperty(Name As String, Value, [DefaultValue])
As you can see, the first argument of the WriteProperty method is the name of the property. The Name parameter references the property name under which the Value argument will be saved. A property value is saved as a Variant. The third argument is a default value. Why provide a default value when saving a property? Before saving the value, WriteProperty compares the default value with the value passed in for saving. If the Value and DefaultValue parameters are identical, then nothing is saved. The property value doesn't need to be saved because default values will be set automatically when the control is reloaded. |
If you consider the great number of properties most controls have, it becomes obvious that storing and then reloading the default values for each and every one of them is time-consuming. It is usually best to define a global constant such as PROPDEFAULT_MYPROPERTYNAME to contain the default value for each property, because you need to supply it in three different places: the InitProperties, ReadProperties, and WriteProperties event procedures.
The ReadProperty method works similarly. ReadProperty accepts the name of the property whose value you wish to read, and returns that value. The optional DefaultValue parameter allows you to specify the default value to be returned in the event that the property you are looking for has not been previously saved. This will always happen the first time the user puts your control on a form, since the user will not have set any properties till that point. It will also happen if the value has never been changed from the default, and therefore never saved by WriteProperty. Consequently, it is a good idea to include error-trapping code in the ReadProperties event procedure to protect your control from invalid property values that may have been entered directly into the .FRM file by overzealous users armed with text editors.
In this part of the series, we breezed through some of the new features in Visual Basic 5.0 and talked in depth about the property features of the object-oriented programming supported by Visual Basic 5.0.
In the conclusion of this series, (March 1997) we will cover methods, property pages, data binding, asynchronous downloading, hyperlinks, and ActiveX documents. We'll also provide some more sample code to demonstrate these new features.
© 1997 Microsoft Corporation. All rights reserved. Legal Notices.