Microsoft’s Internet Explorer provides a convenient, widely available example of a COM-enabled application which supports automation and events. We will use Internet Explorer (subsequently referred to as MS IE) to develop a simple example of using a COMEventSink to receive event notifications from a COM event source object in a VisualWorks application.
The sample application is a simple automation client which launches MSIE through COM Automation and uses a COM event sink to receive notifications for some of the events triggered by the MSIE application.
To install the example, copy the example parcel files COM-MSIEExample.pc* into a directory on your parcel path (or add the directory in which you’ve stored the example files to your existing parcel path) and simply load the parcel named COM-MSIEExample.pcl.
Examples.MSIEEventTraceViewer open
The sample application supports some simple operations for controlling the MSIE web browser that it has launched and uses COM events triggered by the MSIE application to manage its own state and provide trace feedback in varying degrees of detail so that you can monitor the activity of the web browser.
We will work through the process by which this example was developed, to help you understand some of the techniques that you can use when developing your own COM applications using VisualWorks COM Connect and some of the considerations that went into its construction. Note that in addition to demonstrating how to configure and use a COMEventSink to receive COM event notifications, this example also demonstrates developing a simple automation controller for the IE application.
Note: This example was developed using MSIE version 4.0. If you are running a different version of IE, you may encounter difficulties running the example as-is. In particular, IE 3.0 does not support some of the capabilities used in the example application which were added in IE 4.0. The example application was not designed to support multiple versions of the IE application, so if you are running a different version of IE be aware that the MSIE application may not support the interface specifications expected by this version of the sample application. Designing an improved version of the sample application which works with multiple versions of the MSIE application is left as an exercise for the reader. (Hint: The Automation sample applications in COM Connect that demonstrate implementing MS Word and Excel controllers demonstrate an approach for implementing automation clients which work with both the Office 95 and Office 97 versions of those automation applications.)
There is quite a bit of material in the Internet SDK, it turns out, but we discover after looking through the information on the documentation page that the information we need is in the Internet Tools and Technologies (ITT) portion of the SDK, in the discussion of Reusing the WebBrowser and MSHTML.
The Overview section explains the architecture of IE 4.0. The IExplore.exe application is built using the WebBrowser control, an OCX whose object server is implemented by Shdocvw.dll. The WebBrowser control actually provides the web browsing capabilities of the IE application, so we will henceforth focus on the documentation of its capabilities to learn what we can make use of in our IE client application.
The critical information that we need is discussed under Reusing the WebBrowser Control.
This page leads you to the reference documentation on the interfaces and events that we need to know about in order to develop an MSIE client application.
The OLE/COM Object viewer is available with the Microsoft Visual Studio C++ suite.
If you do not already have a copy of this tool, it is quite useful to have when doing COM development.
After exploring the registry a bit to see what’s installed on our machine, we zero in on the Internet Explorer automation object which we confirm is registered to run the Windows IExplore.exe application as the automation object server application for this COM object class. We note that the version-independent ProgID for creating an IE automation object is InternetExplorer.Application and that there is a type library registered on our system which contains the specifications of the component object classes and interfaces of the MSIE application.
Note: If you need more information about ProgID’s and using the version-independent ProgID to reference a COM object class, please refer to the COM Connect User’s Guide sections on automation objects or the Microsoft Automation Programmer’s Reference documentation.
Using the File\View ITypeLib… command, we can open a viewer on the type library specifications for the web browser by locating the shdocvw.dll file in the Windows system directory, as specified in the registry entry for the IE application’s type library. (Recall that this is the type library for the WebBrowser control, as we learned from the IE architecture overview in the InetSDK documentation.) You may wish to use the File\Save As command to save a text copy of the IDL specifications for the type library.
Note: Exploring the type library specifications in detail, we note that the IWebBrowser2 interface actually is defined by the following interface inheritance hierarchy:
IDispatch – standard dispatch interface
IWebBrowser – web browser methods and properties
IWebBrowserApp –
defines methods and properties of web browser Application object
IWebBrowser2
– additional methods and properties introduced by IE 4
The inheritence structure of the interface specifications reflects the historical evolution of the application capabilities. Note also that IWebBrowser2 is a dual interface, should we at some point wish to define a static VTable interface binding to mirror the automation dispatching capabilities.
The primary event interface of the WebBrowser control is an outgoing dispinterface:
DWebBrowserEvents2 – dispinterface specification for the outgoing event interface
ieProgID
^'InternetExplorer.Application'
Confirm that we have the correct ProgID name by evaluating the expression:
GUID clsidFromProgID: MSIEDevExperiments ieProgID
The CLSID value that we see here will be the same value recorded in the registry that you can see in the OLE/COM Object Viewer entry for the IE automation object.
We are going to need to obtain information from the type library, so evaluate the following expression to get the registered ID of the type library:
COMRegistryInterface typeLibraryIDForCLSID: MSIEDevExperiments ieCLSID
Now create a class method in MSIEDevExperiments which records this registered type library ID:
ieTypeLibID
^'{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}'
asGUID
and another which constructs a COMTypeLibrary which we need to have to run the development services which import type library specifications into a form that can be used by the COM Connect automation support mechanisms.
getTypeLibrary
" Answer the type library. The client
must release when done. "
^COMTypeLibrary libraryID: self ieTypeLibID
The key utility that we will use to explore and import information from the type library is the COMAutomationTypeAnalyzer class. This class uses the standard COM ITypeLib and ITypeInfo interfaces to access the type library specifications. It provides a variety of services for creating description reports, specification table literals, and dictionaries for constants. (See the COM Connect User’s Guide for additional information about the services provided by this class.)
To get a quick report on the IWebBrowser2 and DWebBrowserEvents2 interfaces and the constant values defined in the type library, we create some utility methods to execute COMAutomationTypeAnalyzer description services on our type library:
Examples.MSIEDevExperiments
describeIWebBrowser2.
Examples.MSIEDevExperiments describeDWebBrowserEvents2.
Examples.MSIEDevExperiments describeTypeLibraryConstants.
These are indeed the facilities for navigating and browsing that are documented in the Internet SDK for these interfaces, so we’re heading in the right direction. We’re going to need to import the interface specifications for our two interfaces into tables which can be used to configure a COMDispatchDriver and a COMEventSink, so we create some other utility methods to execute COMAutomationTypeAnalyzer specification generation services on our type library:
Examples.MSIEDevExperiments
generateIWebBrowser2Specifications.
Examples.MSIEDevExperiments generateDWebBrowserEvents2Specifications.
These specification table literals will be used with our IE controller to configure the dispatcher and event sinks of our application.
Finally, we want to import the constants that are used for argument values in various interface members into statics under our namespace that can be referenced directly in our IE client application. COMAutomationTypeAnalyzer provides some standard services which create dictionaries for the enumeration types in the type library. However, for our application we decide that we want to create a single static dictionary containing all the relevant MSIE constants, as well as other values that we might want to add. For this purpose, we write a simple utility in our MSIEDevExperiments class which uses a COMAutomationTypeAnalyzer service to create dictionary instances from the type library enumerations and then emits the source code as a file out definition for a single dictionary containing all the constant values from the type library.
Examples.MSIEDevExperiments generateTypeLibraryConstantPoolSource.
Save this source to a file and review the contents. You may wish to prune the enumerations which are not specifically related to the web browser control (e.g., there are several enumerations related to the Windows shell in this type library which are not of immediate interest). When you have customized the variable specifications to the desired set, save the updated source file and then file it in to create the MSIEAutomationConstants dictionary. This dictionary will be declared as a static for the MSIEApplicationController class later.
Note: We also like to have IID constants defined for convenience, so we hand-edit this source file to add definitions for the IID constants IID_IWebBrowser2 and IID_DWebBrowserEvents2, whose GUID values can be obtained from the interface specification tables that our other utilites generate.
We now have all the basic information we need about the MS IE application and its interfaces. It’s time to start creating the real application classes.
Tip: It’s a good idea when doing this kind of exploring to periodically run the COM Resource Browser and release any interfaces that you’ve inadvertantly left lying while while experimenting. Open the resource browser by evaluating the expression:
COMResourceBrowser open
You can release leftover interfaces using either the pane popup menus or the cleanup menu. This will be even more important in a bit, when we make our first attempts to run our own application classes.
The COM Connect controller framework also facilitates working with automation objects which follow the automation architecture guidelines for standard object classes, such as a standard Application object which supports one or more Document objects, or the standard Collection object. Because the IE application follows the guidelines by supporting a top-level Application object, we will create our MSIE controller class as a subclass of the COMAutomationApplicationController framework class. From here, we will inherit convenience methods for the methods and properties of the standard automation Application object.
(In particular, we will end up making use of the inherited isVisible: property accessor and the quit message to shut down the application, as you will see later.)
Create the controller class MSIEApplicationController as a subclass of the standard COMAutomationApplicationController class. Add our dictionary MSIEAutomationConstants to it as a static so that we can access constants when writing convenience methods in the controller class.
Note: Actually, IE only supports some of the standard Application object properties, but not all. Consequently, one approach is to subclass COMAutomationApplicationController and override the inherited methods for properties which not supported by the MSIE application. Alternatively, we could subclass the COMAutomationController class directly and add in copies of the accessing methods for the standard Application properties which are supported. For now, we will simply subclass COMAutomationApplicationController and ignore this issue. But it’s something you would need to resolve properly if creating this class for real use.
Reimplement the standard class methods versionIndependentProgID, typeLibraryID, and getTypeLibrary methods using the information we collected previously and recorded in the MSIEDevExperiments class. These methods provide the fundamental identity information used by the controller framework to create the automation object and dynamically access type library information if necessary.
Most importantly, we need to implement the class method literalSpecification to contain the dispatch specifications for the IWebBrowser2 interface supported by the MSIE application object. The body of this method is supplied by generating the dispatch specifications, as described in the previous section, and pasting the result into the literalSpecification method that we are creating in our MSIEApplicationController class:
Examples.MSIEDevExperiments generateIWebBrowser2Specifications.
Defining these few class methods for the automation object class identity and dispatch specifications is all that we really have to do at this point. However, anticipating some of the facilities that we’ll want to use in our application, we also create instance methods to invoke some of the simple navigation services in the IWebBrowser2 interface: goForward and goBack to navigate the history list, stop to cancel a navigation operation. (That’s good enough for now, we can always come back later and add more convenience methods as desired.)
We should now have everything that we need to support a simple client application of MSIE. To do a basic test of our controller class and its specifications, we will put together a test driver method in our MSIEDevExperiments class which instantiates an MSIE controller (which creates an IE automation object and thus launches the application) and hooks it up in a COMAutomationEditor where we can evaluate expressions to invoke methods and access properties.
To try out our basic controller mechanisms, evaluate:
Examples.MSIEDevExperiments launchWebBrowser
This launches IE and opens an automation editor on the web browser where we can interactively play with the live automation object. While fairly primitive, since only the most basic COMDispatchDriver facilities are available rather than the customized protocol provided by our MSIEApplicationController class, the automation editor can be kind of handy for simple poking about and experimentation with an automation object. The COMDispatchDriver is available as variable ‘dispatcher’. Use the description services in the Developer menu of the automation editor to get a summary report on the dispatch specifications of the MS IE automation object. This is a handy reminder as you invent new expressions to experiment with what you can do with this automation object in your client application.
Note: Be careful with the Quit command which shuts down the application. The automation editor still holds a dispatch interface reference and doesn’t understand the semantics of this operation, so if you invoke the Quit method or shut down the IE window while the automation editor is still open, the dispatcher is left with a dangling (broken) interface which fails with a COM dispatch error on any subsequent attempts to use it.
Note: The launchWebBrowser method you see in the completed example is fancier than the original version, which simply used the basic COMAutomationEditor message openOnDispatcher: to open the automation editor with standard description and text contents values. Once the basic test driver service was working, however, an iterative improvement to the test driver was made to "prime" the editor with some useful expressions specific to the IE automation interface. This is the kind of iterative development that you do in practice but is difficult to capture when presenting a static snapshot of a finished sample application.
The essential step is to import the dispatch interface specifications from the type library and record them in our MSIEApplicationController class so that we have the necessary specifications to configure a COMEventSink to receive the COM event notifications.
Similarly to how we imported the specifications for the IWebBrowser2 dispatch interface for configuring a COMDispatchDriver to control an MSIE application object, we implement a class method literalSpecificationEvents in MSIEDevExperiments to contain the dispatch specifications for the DWebBrowserEvents2 interface supported by the MSIE application object. The body of this method is supplied by generating the event interface dispatch specifications, as we did for the IWebBrowser2 dispatch interface in the previous section, and pasting the result into the body of the literalSpecificationEvents method:
Examples.MSIEDevExperiments generateDWebBrowserEvents2Specifications
Because the controller framework does not (yet) provide a standard framework for defining event sink specifications, create a second class method to construct the actual specification table:
eventSinkSpecificationTable
" Answer the specification
table for an event sink on the receiver's primary event interface."
^self literalSpecificationEvents decodeAsLiteralArray
And that’s really all we need to do to be able to configure and use an event sink for the DWebBrowser2 event interface from an MSIE application. All we need to do now is configure an event sink with these specifications and connect it to an MSIE event source object.
To test this out, we’ll create a second test driver method in MSIEDevExperiments which launches MSIE with an automation editor, as before, and also establishes a COMEventSink on the web browser application to receive COM event notifications that we can watch in a COMEventTraceViewer window. We create the method launchWebBrowserWithEventTraceViewer which starts by using our existing launchWebBrowser test driver to construct the controller and automation editor. Now we will establish an event sink on our controller and open an event trace viewer on in so we can verify that we are indeed receiving COM event notifications from MSIE.
The process of connecting an event sink to an automation object is fairly simple. First, create a COMEventSink object that is configured with the dispatch specifications of the event interface. We create a method constructMSIEEventSink in MSIEDevExperiments which does this, using the eventSinkSpecificationTable method that we just implemented in MSIEApplicationController to configure the sink with the necessary dispatch specifications. Next, the sink needs to be connected to the event source object, which is done in our new test driver method by sending the establishConnectionTo: message to the sink to tell it to establish a connection to our MSIE application object. (The connection can be established using any interface that you already hold on the event source object, which in this case is the dispatch interface of our MSIE application object that our controller obtained when it launched the MSIE application.)
Now you can receive any event notifications of interest in your own application by registering handlers on the event sink using the standard event system services (e.g., when:send:to:). Later, when your application is done, you disconnect the event sink from the COM event source object. We’ll revisit these two steps later, but for now we’ll simply pass our newly connected event sink on by opening a COMEventTraceViewer on it in which we can watch event notifications arrive. (The event trace viewer handles registering for event notifications from the sink and disconnecting the sink when the trace viewer is closed.)
Run the second test driver and use the event trace viewer to verify that COM event notifications are being received from the MSIE application:
Examples.MSIEDevExperiments launchWebBrowserWithEventTraceViewer
Turn on event trace feedback in the event trace viewer using the checkbox when you want to see a trace of incoming event notifications from MSIE. (This can be quite overwhelming, so the trace viewer allows you to turn off the tracing until there’s something that you specifically want to watch.)
If you close IE while the event trace viewer is still open, you will get an error report window open that indicates:
An error has occurred
during a callin to a COM interface function
Error: The object invoked has disconnected
from its clients. ( HRESULT RPC_E_DISCONNECTED )
Further study of the callback stack in the error report indicates that problems occurred while the event sink was being released, apparently by an outside client since we didn’t do this ourselves.
This callin error exposes a slightly tricky area of event sinks. (Which you are free to explore in gory detail using the COM Trace Viewer if you want to see the whole story on all the COM calls coming in and out of our application at the point that IE shut down.) Basically, problems occur when the event source object releases interfaces involved with an open event sink connection because of the mutual references held between the event source and the event sink. The solution is to ensure that you disconnect your event sinks before releasing your last reference to an event source object. Or, in this case, that when you receive a notification of application shutdown that you disconnect any event sinks on the application object that is going away from underneath us.
It turns out that we can handle this shutdown ordering nicely for MSIE by ensuring that an event sink established on an MSIE application always has a handler for the OnQuit event which disconnects the event sink. We arrange for this in our constructMSIEEventSink method by registering an event handler for the OnQuit event which sends releaseConnection to the event sink itself. (Note that this demonstrates the remaining two steps for using a COM event sink: registering an event handler to receive a notification from the event sink when a COM event notification arrives and disconnecting the event sink.)
Now run the second test driver again. This time when we shut down IE the event trace viewer quietly gets disconnected. You should open a COM Resource browser at this point and verify that there are no COM interfaces still in use after you close the automation editor and event trace viewer windows.
Note: If the resource browser shows an event sink still hanging around and it won’t go away even when you release it, you can get rid of the broken sink left over from the original shutdown problem by opening an inspector on it, verifying that it has a reference count of 0, and sending it the message ‘self releaseResources’ to get it to clean up.
What we’ve just learned from developing our test driver has produced some useful services that we should capture permanently with our MSIE controller. In particular, we now go back to MSIEApplicationController and add a class method newEventSink which creates a COMEventSink configured with the MSIE event specifications and an event handler which ensures that the event sink will be disconnected if IE is shut down before our client application has released the event sink. In addition, we create a convenience method on the instance side (also named newEventSink) which answers a new event sink connected to the controller’s MSIE application. So our client application will now be able to work with an event sink simply by creating an MSIE controller and asking it for an event sink on which it can register event handlers for the events of specific interest to the application.
Our MSIE client example is an application class named MSIEEventTraceViewer. Its primary responsibility is to watch for the NavigateComplete2 event so it can report when the browser location has changed. It also watches for the OnQuit event so it can update the user interface appropriately when the IE application has shut down. The application also provides us with several push buttons for invoking some of the simple web browser navigation operations and some check box options to enable tracing some of the events triggered by the IE application. The optional event tracing allows you to observe the more detailed operation of the browser. The CommandStateChange event from the web browser is also monitored by our application so that it can properly enable and disable the forward/back operation buttons it provides for stepping through the browser’s history list.
First run the application and try it out, then we’ll discuss some of the key parts of its implementation. To run the MSIE event tracer evaluate the expression:
Examples.MSIEEventTraceViewer open
The application open methods in MSIEEventTraceViewer use the same mechanisms that our test drivers used to launch the MSIE application by creating a MSIEApplicationController to control the MSIE application object and connect a new event sink to receive COM event notifications. Recall that the event sink services that we created in MSIEApplicationController handle all the COM "plumbing" mechanics on configuring the event sink’s dispatch specifications, connecting the sink to the COM event source object, and disconnecting the sink when the IE application terminates. This allows our application to focus on what we’re interested in, which is performing simple operations to control the IE application and monitoring specific events that we are interested in processing.
Most of the interesting instance methods in this application are found in the ‘private-operation’ and ‘private-browser events’ categories, where you will find the methods which register and remove event handlers on the COM event sink and which implement the event processing of the MSIE events for our application. When the application window is opened, the registerPermanentEventHandlers method is called to register event handlers for the CommandStateChange , NavigateComplete2, and OnQuit events that we always want to monitor. When the application window is about to be closed, the releaseBrowserConnections method is called to ensure that we release the event sink and the browser controller now that our client application is done using them. Remember that releasing the event sink will have already been taken care of if the IE application has been closed while our application is still open, but if the web browser is still open when our window closes then we need to release the COM resources that our application still holds.
The NavigateComplete2 event provides a good example of a basic COM event handler. Our application registers a event handler to monitor this event when its window is opened with an expression of the form:
self eventSink
when: #NavigateComplete2:_:
" with: <anIDispatch> with: <url> "
send: #navigateCompleted:to:
to: self.
The navigateCompleted:to: method is now invoked whenever a COM NavigateComplete2 event notification is received from the IE application. Our event handler is quite straightforward: it simply logs an entry in the event trace log to record the new location of the browser. However, do take note of the URL argument for this event: it demonstrates a typical case where you must pay attention to the declared parameter types of an interface member to properly manipulate the argument value. Because the type library declaration of the URL parameter for this event declares it to be VARIANT*, the argument is received from the event sink as a reference to the (string) value. Consequently, we must dereference this argument by sending the value message to obtain the actual value of the URL.
Hint: You can spot reference arguments easily in the dispatch specification table by looking for arguments with the VT_BYREF type modifier.
The action button methods for the browser navigation operations in the ‘private-user actions’ method category (go forward/back in the history list, stop a navigation that is currently in progress) are straightforward dispatch method invocations using the convenience protocol provided by our COMAutomationApplicationController wrapper class.
When one of the check boxes which control trace reporting of various kinds of events from the IE application that we might want to monitor is clicked by the user, the appropriate toggleXXXTraceSetting method registers or removes the relevant event handlers on the event sink. For example, when we want to monitor updates to the status text in the IE application we register an event handler for the StatusTextChange event:
registerStatusEventHandlers
" Private - register
a handler for status events supported by the IE application that we want
to trace."
self eventSink
when: #StatusTextChange: " with: <text> "
send: #browserStatusTextChanged:
to: self.
When we no longer are interested in monitoring the status text updates, we remove the handler from the event sink:
removeStatusEventHandlers
" Private - disable
tracing by removing the event handlers. "
self eventSink
removeActionsWithReceiver: self
forEvent: #StatusTextChange: .
Monitoring progress updates and the various navigation events are done similarly.
One other event handler implementation in our application is worth exploring a bit further, as it demonstrates how to veto a cancellable event. The BeforeNavigate2 event has a number of arguments, the last of which is a reference to a boolean cancel flag that an event listener can use to veto the proposed operation. When navigation event tracing is turned on in our application, the method proposeNavigate:to:flags:…cancelFlag: [it’s a long keyword with a lot of selectors, you’ll find it easily] is registered as the handler for the BeforeNavigate2 event. This event is triggered by the MSIE application when a navigation operation to follow a link is about to be initiated. The event arguments include all the arguments for the proposed navigation operation. Just as with the NavigateComplete2 event, note that most of these arguments are declared in the interface member declaration as being passed by reference, so we have to dereference the arguments to obtain the actual value of interest, such as the destination URL.
Our handler for the BeforeNavigate2 event asks you if you would
like to allow the navigate operation to proceed. If not, we veto the proposed
operation by setting the cancel flag of the event. When IE examines the
cancel flag after triggering the event, it learns that the proposed operation
has been vetoed and leaves the browser at the current location.
Links to:
The
Cincom Smalltalk web site
Cincom
Smalltalk Developer Links
Cincom
Smalltalk 's Online Documentation Site