Basically Visual

January/February 2000

by Peter G. Aitken (c)2000

Originally published in Visual Developer magazine


ActiveX Documents, Part 2

In my previous column (available here in case you have not yet read it - shame shame!) we started looking at one of Visual Basic's powerful Internet technologies, ActiveX documents. An ActiveX document is, in effect, a full-featured Visual Basic application that is deployed over the Internet and runs inside a container, usually a web browser. Despite the "document" part of its name, an ActiveX document is better described as a hybrid between an application and a document, containing both executable code and data. ActiveX document technology offers you the full power of the Visual Basic language and its visual interface design tools coupled with the capability for web-based deployment and updating, something that is become more and more important these days. There are tradeoffs, of course. The technology's hefty download requirements and limited browser compatibility limit its usefulness primarily to internets. Still, that leaves a huge number of places where ActiveX document technology may well be your best bet for web development.

 Container Differences

At present, ActiveX documents can be "run" in three different containers: Internet Explorer versions 3 and greater, Microsoft Office Binder versions 1 and later, and the Visual Basic development environment tool window. While ActiveX document applications are often written with a particular container in mind, the possibility exists for applications to be written for multiple containers. This is, I believe, an exciting possibility, particularly as more and different containers become available.

That being said, it does complicate programming to some degree. While a container capable of hosting an ActiveX document must, of course, meet a minimum set of capabilities in order to do so, the different containers will unavoidably differ in various ways that are potentially relevant to an ActiveX document that is running in them. One example is navigation. In Internet Explorer, you navigate using the Hyperlink object's NavigateTo method, whereas the Binder requires that you add a new Binder section, and the code is completely different. This means that your ActiveX document applications need to check which container they are running in. Even if your application is intended to run only in a particular container, you may want to have start-up code that checks the container and, if it is not the proper one, displays a message to the user and terminates.

To determine which container an ActiveX document application is running in, use the TypeName function, which returns information about the variable passed as its argument. If the variable is a reference to the ActiveX document's parent, obtained as UserDocument.Parent, then the function returns a string identifying the container, as follows:

Container                                           Return value

Internet Explorer                               "IWebBrowserApp"

Binder                                                 "Section"             

Visual Basic tool window                 "Window"

 At least, this is what the Microsoft documentation says is supposed to happen. I have noted, however, that when Internet Explorer 5 is the container, TypeName returns the string "IWebBrowser2." My guess is that if the return value includes the text "browser" then the container is some version of Internet Explorer. Then, if your ActiveX document project is intended to run only within a browser, you could put something like the following in each document's Show event procedure:

If InStr(1, TypeName(UserDocument.Parent), "browser", _
   
vbTextCompare) = 0 Then

    ' Code here to display message and terminate application.       

End If

Public Properties, Variables, Methods, and Procedures

A public property, variable, method, or procedure is one that is available throughout the entire ActiveX document project, and not just in the document where it is defined. Once you go beyond the basics with ActiveX documents, you'll find that multiple-document projects are often the easiest way to accomplish your task, and these multiple documents often need to "talk" to each other. We'll get to why they might need to talk to each other in a minute, but first the how. If you are familiar with making items public in ActiveX controls, this will all seem very familiar to you.

To make a procedure or a variable (in a code module) or a method or a property (in a UserDocument) public is simply a matter of using the Public keyword in the definition or variable declaration. Public is the default for regular and property procedures, and for methods (which, of course, are just procedures under another name), so these code elements will always be public unless you explicitly make them private. Then, call them from other modules using the procedure name or, for a method, the standard object.method syntax. More important, as we'll soon see, is creating public variables in code modules. By declaring a variable at the module level (outside any procedures), using the Public keyword, results in a variable that is visible throughout the project.

When do public items in an ActiveX document project become available? Items - public  procedures and variables - in a code module are always available. Things are a bit more complicated when it comes to public methods and properties of a UserDocument. Anything within a UserDocument becomes available when an instance of the document is created, which happens when the document is loaded into a container. Then, the items go out of scope - become "unavailable" - when that instance of the document is destroyed, which usually happens when it is unloaded by its container. I say "usually" because there are differences between containers in terms of when a document instance is actually destroyed. Because some containers do, in fact, destroy a document instance as soon as it is unloaded, you should write your code as if this is always the case.

Perhaps you can already see the potential for problems. The whole idea of public properties and methods in one document is to make them available to code in other documents, right? Thus, Doc1 might contain properties and methods that are accessed by code in Doc2. What happens if Doc2 runs before Doc1 is loaded, or after Doc1 has been unloaded and destroyed? Remember, in an ActiveX document project you do not have the same close control over when things are loaded and unloaded as you do in a regular Visual Basic application. Any ActiveX document can be navigated to independently, and your coding must take this possibility into account.

The best solution is to keep a separate, global reference for each document in the project. These references are created as global variables in the project's code module. For example, suppose your project contains three documents. Then, at the module level in the project's code module you would place the following public variable declarations:

Public gDoc1 As Object
Public gDoc2 As Object
Public gDoc3 As Object

Then, within each document, add code to set the global reference to that document. For example, somewhere in the code in Doc1:

Set gDoc1 = Me

Where exactly this code would go depends on the details of your project. If you always want the reference to the document saved in the global variable, then the code could go in the document's Show or Terminate event procedure. If the reference needs to be set only under certain circumstances - say, when the user navigates away from this document to another specific document - then the code could be placed along with the navigation code. In either case, the point is that, since a reference to the document has been saved in a global variable, the instance of the document will not be destroyed when it is unloaded from the container but will be available as long as the application is running. Hence, the document's public properties and methods will remain available.

"But wait," you may be thinking, "what if Doc1 has not been loaded at all when another document's code references it?" Good question - but the point of this technique is not to guarantee that an instance of Doc1 always exists, but to permit other components of the project to determine whether an instance exists and then take appropriate action. This is done simply by querying the value of the global variable - gDoc1 in this case. If its value is Nothing, then an instance does not exist and the code can take appropriate action - including creating an instance, if required. If, on the other hand, the value of gDoc1 is not Nothing, then an instance of the document exists and its public methods and properties can be used.

Here's an example. Suppose you want your project's various documents to have a consistent appearance, but also want to permit the user to set their own foreground and background colors as desired. When Doc2 is loaded, you want it to use the same background and foreground color as Doc1, but only if Doc1 has already been loaded (which means that the user may have customized its colors). If Doc1 has not been loaded, then Doc2 should use the default foreground and background colors. The following code in Doc2's Show event procedure does the trick.

Private Sub UserDocument_Show()


   
If gDoc1 Is Nothing Then
       
BackColor = DEFAULT_BACKCOLOR
       
ForeColor = DEFAULT_FORECOLOR
   
Else
       
BackColor = gDoc1.BackColor
       
ForeColor = gDoc1.ForeColor
   
End If

End Sub

By utilizing this same general technique, with variations to suit the occasion, you can take make use of the power of a multi-document project without running into problems trying to reference a non-existent object. It is also possible to use global code module variables to pass data between one document and another. I have found this useful for small bits of data, such as maintaining application-wide flags. Larger chunks of data are better passed between documents using the file system.

Remember that good programming practice requires that you destroy object instances when they are no longer needed. If you are keeping global references, this is particularly important, because you cannot count on your document objects being destroyed automatically when unloaded. To avoid hogging system resources with no-longer-needed objects, while retaining objects you will refer to later, you may need to do a little bit of planning.

Viewports

What guarantee do you have that the container's viewing area will be big enough to display your entire ActiveX document? None at all. To deal with this situation, container objects provide for scrolling to bring different parts of a too-big document into view. The "window" through which you view a part of the document is called the viewport. When the document is longer and/or wider than the viewport, the container automatically displays a vertical and/or horizontal scrollbar to permit the user to scroll. If the user changes the size of the container, the scroll bars appear and disappear as needed.

The automatic display of scroll bars by the container is controlled by the UserDocument's ScrollBars property. The default setting is for both scrollbars (horizontal and vertical) to be displayed as needed. You can change this property to display only one, or no, toolbar, but I cannot imagine a situation where this would make sense.

When the application is running, code in the ActiveX document can obtain information about the current viewport - its position relative to the UserDocument and its size. In effect, this permits your code to determine what parts of the document are visible, and what parts are not visible, at any given time. This information, in units of twips, is available from the following UserDocument properties:

ViewPortWidth
ViewPortHeight
ViewPortLeft
ViewPortTop

As with all default coordinate systems in Windows, the top left is the origin, with coordinates 0, 0. What can you do with this information? Depending on the nature of your document, you may be able to resize it so it fits perfectly within the container viewport, freeing the user from the need to scroll to see all parts of it. If the document cannot be resized - perhaps it contains a collection of carefully placed controls - you can scroll the document under program control to bring a particular part of it into the viewport. This code shows an example. When the specified text box gets the focus, the viewport is shifted to move that text box to the upper left corner of the viewport.

Private Sub Text1_GotFocus()

UserDocument.SetViewport Text1.Left, Text1.Top

End Sub

Note that this works only if the viewport is smaller than the document when the focus is moved.

Asynchronous Data Transfer

Asynchronous data transfer permits an ActiveX document to obtain data from a remote source, whether it be a file or a URL. Because the transfer is asynchronous, it happens in the background while your ActiveX document application is busy doing other things. When the transfer is complete, an event fires notifying the program. Another event fires as the download progresses, giving the program access to the data as it arrives.

You use the UserDocument's AsyncRead method to initiate an asynchronous data retrieval. The syntax is:

AsyncRead Target, AsyncType, PropertyName, AsyncReadOptions

Target is a string expression that specifies the data to retrieve. It can be a URL or a path to a file.

AsyncType specifies the format of the data being retrieved. Use VbAsyncTypeFile if the data is a file created by Visual Basic, VbAsyncTypePicture if the data is a Picture object, and VbAsyncTypeByteArray if the data type is unknown (an arbitrary sequence of bytes with no assumptions as to its structure).

PropertyName is an optional argument where you assign an arbitrary name to the download. The name you assign has no effect other than to permit you to identify the download process if two or more downloads are ongoing at the same time.

AsyncReadOptions is an optional argument that specifies certain download options, mainly having to do with server versus locally cached copies of the data. Possible settings are explained in Table 1.

Table 1. Settings for the AsyncRead method's AsyncReadOptions argument.

Constant (value)

Description

VbAsyncReadSynchronousDownload (1)

The download is done synchronously, which means that execution does not return from the call to AsyncRead until the download is complete.

VbAsyncReadOfflineOperation (8)

The locally cached copy (if present) is used.

VbAsyncReadForceUpdate (16)

The remote copy is retrieved regardless of whether a local copy is available.

VbAsyncReadResynchronize (512)

The remote copy is retrieved only if it is newer than the locally cached copy.

VbAsyncReadGetFromCacheIfNetFail (524288)

Use the locally cached copy if there is a network problem; otherwise retrieve the remote copy.

What exactly happens when you call the AsyncRead method? Errors can occur with this method, and they occur synchronously, which means that you should have error trapping enabled when you use this method. Absent an error, the data transfer begins and, assuming you did not specify a synchronous download with the VbAsyncReadSynchronousDownload option, program execution continues with the statements following the call to AsyncRead as the down load proceeds in the background. Two events fire, AsyncReadProgress as the download progresses, and AsyncReadComplete when it is finished. When the download is compete, the data are available in a disk file for your program to use as needed.

Both of these event procedures receive a single argument, of type AsyncProperty. This is an object whose properties provide information about the download at the time the event fired. These properties are explained in Table 2.

Table 2. Properties of the AsyncProperty object.

Property

Description

AsynchType

The type of data being downloaded. Possible values are the same as for the AsyncRead method's AsyncType argument, as explained above.

BytesMax

An estimate of the total number of bytes to be downloaded, as a type Long.

BytesRead

The number of bytes read so far, as a type Long.

PropertyName

The PropertyName argument passed to the AsyncRead method, uniquely identifying the download.

Status

A string describing the current status of the download.

StatusCode

A numerical code giving the current status of the download operation. See Table 3.

Target

The target argument passed to the AsyncRead method.

Value

The name of the local file where the downloaded data is placed.

 Table 3. Status codes provided by the AsyncProperty object.

Constant (value)

Meaning

VbAsyncStatusCodeError (0)

An error has occurred. The Value property contains information about the error.

VbAsyncStatusCodeFindingResource (1)

The target is being located.

VbAsyncStatusCodeConnecting (2)

A connection is being established with the target.

VbAsyncStatusCodeRedirecting (3)

The method has been redirected to another location.

VbAsyncStatusCodeBeginDownloadData (4)

The download has started.

VbAsyncStatusCodeUsingCachedCopy (10)

Data is being obtained from a local cached copy of the target.

VbAsyncStatusCodeSendingRequest (11)

The method is requesting the target.

VbAsyncStatusCodeMIMNETypeAvailable (13)

The MIME type of the target is available, and is specified in the Status property.

VbAsyncStatusCodeCacheFileNameAvailable (14)

The filename of the local cached copy of the target is available in the Status property.

VbAsyncStatusCodeBeginSynchOperation (15)

The download is being done synchronously.

VbAsyncStatusCodeEndSynchOperation (16)

A synchronous download has completed.

With these two events, you can probably see the basic structure of the code required to perform a data transfer. The basic outline of the required steps are:

1. Call the AsyncRead method, passing the appropriate arguments for the data being retrieved. Be sure to trap errors.

2. In the AsyncReadProgress event procedure, place code to examine the properties of the AsyncProperties object. It is optional to keep the user informed of the progress of the download, but it is strongly advised to perform error checking here, keeping a lookout for a value of VbAsyncStatusCodeError n the StatusCode property.

3. In the AsyncReadComplete event procedure, place code to inform the user that the download is complete, and to take whatever actions are required with the downloaded data.

You can see how the ability to asynchronously read data from a URL could be a very powerful tool. But why would you want to use AsyncRead to read data from a remote file on a local network? Visual Basic's regular file access statements are suitable for that task. The answer lies in the asynchronous nature of the transfer. Even local networks can experience congestion and delays, and since the regular file access statements operate synchronously your program will "hang" while a slow file access operation is in progress. With AsynchRead you avoid this potential problem.

Menus in ActiveX Documents

ActiveX documents begin to seem even more like regular Visual Basic applications when you realize they can have their own menus. When you are working on a UserDocument in the Visual Basic development environment, you use the menu editor to create you menu, just as for a regular Visual Basic form. When the document is loaded into its container, its menu is combined with the container's own menu, a process known as menu negotiation.

When you are using the menu editor, you'll see a NegotiatePosition property associated with each menu item. This property is relevant only for top-level menu items, and cannot be changed from its default setting of 0-None for subitems. When applied to a top level menu item, the possible settings and their effect are:

0 - None. The menu does not display in the container.
1 - Left. The menu is displayed at the left on the container's menu bar.
2 - Middle. The menu is displayed in the middle of the container's menu bar.
3 - Right. The menu is displayed at the right on the container's menu bar.

When the document is displayed in the container, the final menu arrangement depends on both the Caption and the NegotiatePosition properties of the document's top-level menu items. There are three possibilities:

- If the container does not have a top level menu item with the same caption, then the document menu will have its own position on the menu bar, at the position specified by the NegotiatePosition property.

- If the container does have a top level menu item with the same caption, and it is at the same position as the document menu item's NegotiatePosition property, then the document menu item will be merged with the container's menu item.

- If the container does have a top level menu item with the same caption, and it is not at the same position as the document menu item's NegotiatePosition property, then the document menu will have its own position on the menu bar, at the position specified by the NegotiatePosition property.

This may seem a bit confusing, so let's look at an example. If your document has a top level menu with the caption Help, with NegotiatePosition set to 3 - Right, then it conflicts with the InternetExplorer menu and therefore will display as a subitem on Internet Explorer's Help menu (with the caption XXXXHelp, where XXXX is the name of the UserDocument). If however the NegotiatePosition property is set to 2 - Middle there is no conflict because Internet Explorer does not have a Help menu item in the middle of the menu bar. In this case the document's Help menu will display separately on the menu bar.

Depending on the container, it may also be possible to manipulate the container's menu (although this is not covered in this column). This provides you with the possibility of completely customizing the container menu so the user sees only those menu commands that you want them to have access to - always a good idea to keep careless users from getting themselves into trouble!

Converting Existing Applications to ActiveX Documents

In the long standing Visual Basic tradition of making your life easier, there is an automated way for you to convert existing Visual Basic applications to ActiveX document applications. After all, since an ActiveX document is, in effect, a Visual Basic application in a different suit of clothes, there are many situations where converting an existing application to an ActiveX document makes a lot of sense. The tool is called the ActiveX Document Migration Wizard, and it is an add-in that is accessed with Visual Basic's Add-Ins menu. The wizard does not change the existing application, but creates a new ActiveX document project based on it. Some of the things it does are:

- Copy properties, controls, and code from forms to UserDocuments.

- Comments out illegal code, such as the End statement.

- Removes any OLE container controls and embedded objects (such as Excel spreadsheets), which are not supported in ActiveX documents.

For event procedures, where an exact counterpart exists, the wizard copies the code and changes the procedure name. For example, Form_Click is changed to UserDocument_Click. Where there is not an exact counterpart, the event procedure is copied with its name unchanged. This, the Form_Load event procedure is copied with its name unchanged and can be called as a general procedure, if needed.

Note that the process of migrating a standard Visual Basic application to an ActiveX document project is not completely automatic - after the wizard does its work, you'll still need to work on the project a bit. However the wizard certainly saves a lot of time.

In Conclusion

With ActiveX documents, I think we get a glimpse of the future of application development. Even more, we get a glimpse of the direction that computing in general is headed. The Internet has only begun to change things, and as the "wired world" continues to evolve, the distinctions between local and remote, here and there, will blur even further. Developers will have more choices, of course, but equally important is that they will be freed from other choices because they will no longer matter. The dichotomy of whether an application is deployed locally or remotely, and whether it executes locally or remotely, is already loosing some of its meaning. ActiveX documents are a reflection of these changes, and a technology you can put to work today.