Build OLE Controls for the Internet that are Fast, Smart, and Interactive
Michael T. McKeown
Michael McKeown specializes in OLE and client technology. He speaks at conferences, and wrote verification software for the OLE Control and Control Container Guidelines found on MSDN.
From the flood of OLE-enabled applications released with Windows®95, it is quite clear that OLE Controls are being implemented ubiquitously throughout desktop applications. Solution builders have come to realize that the control programming model lends itself to quick development of integrated component solutions. It is therefore only natural that controls are finding their way onto Internet Web pages as a means of enriching user interaction. The design of some controls allows them to make the transition from a local machine to the Internet transparently. Others may need to take certain considerations into account, such as code size and management of data, to make them more efficient for use over a relatively slow communications link.
In an attempt to remove some of the confusion about using controls on the Internet, let's be clear in understanding that there is no such thing as an "Internet" control. Any existing desktop control can be used to create active content on an HTML page. However, a control operates much more efficiently during downloads if its code is small and it loads large property BLOBs (like an MPEG file) asynchronously. This should occur in the background as the page and the controls are being transferred from the Web server to the user's machine.
In this article, I'll show you techniques for making your OLE controls work well on the Internet. You'll learn about BLOBs, URLs, persistence, and property bags. You'll also learn how to put your "fat" controls on an ultra-slim diet. I'll wind up with a real example that illustrates cooperative downloading between browser and control.
The newly revised "OLE Control and Control Container Guidelines v. 2.0" defines an OLE control as nothing more than a COM object that is self-registering (exports DLLRegisterServer/DLLUnregisterServer) and implements the IUnknown interface. That's right, just IUnknown! No more IDataObject or IOleControl-that stuff is too much extra baggage for Internet controls, which must be slim and sleek. Controls are therefore free to implement only the interfaces they need to perform. This may lead to a drastic reduction in code size, which means quicker download times.
Of course, a control that implements only IUnknown is useless. To serve any useful purpose, you need more interfaces. Under previous versions of the guidelines, certain interfaces and methods were categorized as mandatory. The container (the browser in this case) could depend upon them being there when it created the control. But now that all the old interfaces are optional for a control, how does the container/browser know which interfaces the control supports? Even if the container does know which interfaces exist, how does it know what specific capabilities the object possesses?
Component categories are a means of describing what a control does rather than merely listing the interfaces it supports. Controls can declare their functionality to prospective container apps, as well as any support they require from the container/browser. Note that, since there is no longer any difference between a COM object and an OLE control, categories can apply to any OLE object, not just controls. Each component category carries a description of the functionality required to be associated with this category as well as a unique Category ID identifier (a GUID) for that category. Microsoft is developing a standard set of component categories and their descriptions, just as it provides a standard set of OLE interface definitions (see the Component Categories specification). These categories are not all-encompassing; they can be extended as needed.
Component categories let an object describe its functionality to clients in a richer way than merely through its interface signature. This means that clients can uncover an object's capabilities without actually creating it and calling QueryInterface. For example, suppose you're writing a container and you want to determine if a particular control can convert data into the XYZ format. In the old days you would create an instance of the control, QueryInterface for the IDataObject interface, then ask for XYZ format. Creating the control is an expensive operation, and even if it succeeds, the control may not support XYZ.
Component categories solve this problem. Each component category is represented in the registry by its Category ID and its associated human readable keys (as well as using custom data tags within its type library). So in the new world, the control registers itself as an "XYZ converter" and your container can detect this without ever creating an instance of the control.
New OLE interfaces let you register/unregister, enumerate, and query Category IDs in the registry. Control containers can search the registry for only those controls that provide the functionality the container requires-like XYZ converters. Containers typically do this to generate a selection menu, like in an Insert Object dialog. Containers can also filter out controls that require functionality the container does not support.
Previously, the "Control" keyword in an object's registry entry made it eligible to be used in a programming tool like Visual Basic. "Control" announces the object's ability to function in the role of a control, but does not provide any other information regarding the control's capabilities. Component categories were introduced to enhance this identification process. The "Control" keyword is now obsolete, but remains for backward compatibility. You can still add it to the registry entry for controls you want to be hosted in older-style containers, such as Visual Basic® 4.0, that don't understand component categories.
Many of the controls in use today-including a majority of those that ship with Microsoft products-were created with the Microsoft® Visual C++® Control Wizard. This tool was designed to allow programmers who know nothing about OLE to create great controls. While Control Wizard made OLE controls easy, its COleControl-based model incorporates a lot of the old interfaces that may not be required in every situation. From an Internet perspective, Control Wizard generates fat controls. Additionally, these controls require the extra overhead of MFC DLLs to run.
Keep in mind that "fat" is a relative term. On the desktop, Control Wizard-generated controls are fine since all the code and DLLs live on the local machine. Across the Internet, however, size and MFC DLL dependencies can be a significant detractor for Web authors considering using your control. They don't want their page to take a long time to download; impatient users will simply abort the process and move on.
Before proceeding, I'd like to clear up some of the misconceptions about using MFC-based controls on a Web page. A Visual C++ 4.0 MFC-based control is by no means necessarily too large for effective download in all cases. A release build of a default Control Wizard-generated control is only 22K, which is an acceptable size for some situations. It's just that you can do much better than that if you want. Note that there will be various controls that are common
to many Web pages and therefore will only have to be downloaded once for the first page that uses it and not for every subsequent page that uses it. It's a onetime performance hit.
For MFC-based controls, if the MFC DLLs are already resident on the user's local machine, there's no reason to download them. Of course, with MFC releases happening every three months, new MFC DLLs may appear on Web sites faster than users can download them. As with the control, though, the user has to download the DLLs only once.
The Visual C++ and MFC teams are hard at work on a new Control Wizard that generates skinnier controls, which will perform well over the Internet. There will also be Class Wizard support for data path properties (which I'll describe later), allowing you to persistently store the URL addresses of these large BLOB properties. Support for conveying a control's readiness state to its container will be provided. In many cases this will entail little or no work on the part of the control developer. In general, you will be able to take advantage of other improvements in Control Wizard and Class Wizard to help develop Internet apps, and new MFC classes will wrap the interfaces associated with asynchronous downloads.
Controls developed using these new versions of MFC will have significant performance improvements over controls written using MFC 4.0. In particular, MFC will expose some of the new optimization features specified in the OLE Controls '96 Specification, such as windowless controls. In future versions, the MFC DLL issues will be improved to make Internet distribution of your control simpler and faster. Additionally, MFC will eventually support other Internet-related technologies including OLE Hyperlinks, OLE DocObjects, WinInet, and Windows Sockets 2.0.
The next release of Visual Basic should include the long-awaited ability to create OLE controls. (Hooray!) A new product called Visual Basic Internet Control Pack contains controls to add Internet connectivity to your Visual Basic-based app now. The controls in Visual Basic Internet Control Pack do not use Visual C++ and MFC; rather, they inherit from a new control class-which, for want of a better name, is called the Win32® base control class-included with the new Internet-enhanced Win32 SDK. (These SDK enhancements, called Sweeper, represent Microsoft's Internet client strategy and are not particular to any one product.)
The Win32 base control class lets you create small and refined controls that implement only those interfaces that you require. Controls based on this class are also self-supporting; there are no additional DLLs to lug around with the control. All of the source code is included, and the class is designed with extensibility in mind so you can customize it easily. To help you understand how it works, a few sample controls and a good README.TXT file are included with the SDK.
There's just one catch: you have to understand OLE. In particular, you really need to understand OLE Automation and dual interfaces, how persistent interfaces work, and how to create and maintain ODL files (not trivial). It's not like Control Wizard, where you check a few radio buttons. These new controls will require more knowledge and more coding on your part. So how does a developer decide which option to use to create controls? The diagram in Figure 1 will assist you in the process.
Figure 1 Choosing Your Object
If you can't wait for updates to Visual C++, MFC, and Visual Basic-if you want Internet controls now-then you may want to use the Win32 base control (BaseCtl) supplied with the Sweeper SDK. This is generally for those developers who have specific needs to significantly reduce their control's working set size, do not need to use the UI and activation support that MFC provides, have to download large property BLOBs for their persistent control properties, and are familiar with the pertinent OLE technologies described here. Unless you fall under one or more of these categories, the Visual C++ Control Wizard is the easiest and quickest way to develop controls today (and it will be much better very shortly).
As a Webmaster designs a page that includes an Internet control, he or she will most likely set certain properties for that control from its property page within the authoring tool. For example, a control that plays an MPEG video may have a Title property to display the title of the MPEG file while it's playing. It may also have a Border property that can be set to different colors. Of course, the Webmaster must tell the control which MPEG to play by setting its MPGfile property.
When the author decides the page design is complete and wants to save it, the authoring tool must allow some way to store all these property values. If the property data is small-a hundred bytes or less-the data could be stored directly inline within the HTML code by using the DATA attribute of the <INSERT> tag. If the data is large-up to 10KB-it is better to save it in a separate file. In this case, the DATA attribute specifies the URL address of the file. Each control has its own property data independent of all other controls on the page (and other pages). This means separate files on the server, or separate inline, base 64-encoded DATA streams within the HTML source.
Controls are responsible for saving their own properties, if there are any. (MFC-based controls today accomplish this through the COleControl::DoPropExchange method.) The control dictates what is saved in the persistent stream when the author selects something like "Save to Stream" from the authoring tool. Other information may also be stored, such as extended properties from the authoring tool (Align, Height, #, and so on) and the CLSID of the control. The theory of persistent storage is to let the control initialize itself on the user's machine at run time using this persistent data so it can recreate its state exactly as it was when the author saved it on the server Web site.
When the page is downloaded, the persistent data for each control must be transferred across the communications medium. In my example MPEG control, its CLSID, Text, and Border values do not present a problem since they take no more than fifty bytes or so. If the MPEG file is bigger (a typical MPEG file is 1-2 MB), users may end up taking an unplanned siesta-it may take a while before anything can be done with that page. Chances are the user will abort the download and never see the page. Everybody loses: the user doesn't get what he or she wants; the Webmaster never gets the message to the user; and the control gets a bad reputation.
What would be ideal from the user's point of view is to get the page quickly, then load the MPEG file in the background-just like pictures work today. This prevents the user interface from becoming frozen while waiting for everything to download. The user may never play that MPEG file, so why should everything have to wait for it?
The solution is to store small property values separately from large property BLOBs (see Figure 2). You can keep small items inline, like Border, but move large BLOBs to a separate file by defining them as data path properties within the type library of the control.
Figure 2 Three Elements of a Transfer
Data Path Properties and Monikers
A data path property is nothing more than a string that contains either the absolute or relative URL address of the data file. It's implemented as a GUID-typed alias for the type
OLE_DATAPATH (alias for BSTR), and contains a link to the external data BLOB managed by the control. These strings are stored just like any other persistent property, such as a Border or Title.
Figure 3 shows how a control describes its data path properties to containers in its type library. Data path properties must always be marked as bindable and requestedit. This allows the container to update the value of the URL for the data path property if the Webmaster associates it with another file at design time. When it's time to download the BLOB, the browser/container creates a URL moniker with the data path value. It then passes the moniker to the control, which calls BindToStorage to load the BLOB asynchronously. (This is discussed in much greater detail later.)
Before we delve further into data path properties, however, let's take a moment to make sure we all understand monikers. In the traditional definition, a moniker is a name object that encapsulates the ability to find and deliver the object or storage it names. It has the ability to store persistent data that it uses to bind to the object or file it names. "Binding" is the process of locating the object the moniker names, creating an instance of it in memory, and returning an interface pointer to it. You don't have to implement monikers (unless you want to do something special); monikers are part of Windows.
Just as a filename names a stream of bytes stored on disk, a moniker names an object (see Figure 4). However, where a filename is dumb (it does nothing), a moniker is smart. You tell it to bind to an object (or storage) and it delivers a pointer to that object (or storage interface). Once this delivery is complete, the moniker is not needed anymore and is released.
Figure 4 Monikers Naming BLOB Properties
Traditionally, monikers bind synchronously; that is, BindToStorage does not return until all of the data has been placed in its storage object. This model doesn't work well over the Internet, where bandwidth is scarce. This is precisely the motivation for asynchronous monikers. BindToStorage returns immediately, allowing the data to trickle down in the background.
Asynchronous monikers are a paper specification of a class of OLE monikers that do asynchronous binding to objects or storage. Currently, URL monikers are the only implementation of asynchronous monikers. The URL moniker provides more automatic management of downloads using the HTTP, FTP, and Gopher protocols than do the lower-level WinInet APIs or Windows Sockets.
During binding, monikers use something called a "bind context." The bind context serves many purposes: it contains parameters for the binding operation, holds references to be activated when a moniker is bound, and helps the moniker retrieve information about its environment. There is one bind context associated with each binding. Both synchronous and asynchronous monikers are derived from IMoniker. The only difference is how they make use of the bind context to bind data in either a synchronous or asynchronous fashion. No new methods or parameters have been added to the IMoniker interface for asynchronous monikers.
The ability of a control to maintain external references to data (via monikers) was always available in the past with OLE Documents, except that the links were strictly internal to the object and were unknown to the container. This prohibited containers from managing or assigning storage for them. This is unacceptable in the context of the Internet, where browsers may need to coordinate the interconnections among many Internet sites concurrently. With data path properties, external data references are exposed in a standard way that lets the container and control cooperate in managing retrieval of external BLOB data.
Once the control is initialized with the small property values, and just prior to returning control to the container, the control can start downloading its data path (BLOB) properties in the background asynchronously. This lets the page become at least partially active quickly, as opposed to waiting for all controls to finish their respective downloads. The control and control container receive regular update notifications from the system as to the status of the BLOB download. A UI mechanism can be displayed by the container to show the user the cumulative percentage of data downloaded for all of the controls on the page. At various stages of the process the control may want to notify the container of its ability to function. When the process is complete, the control tells the container and is ready to be interacted with fully.
Interfaces for Asynchronous Downloads
I want to show you a key subset of the interfaces and methods that both the container and control should support to do asynchronous property downloads of data path properties. This is not a complete list so please refer to the OLE Controls/COM Objects for the Internet and the Asynchronous Moniker specification for additional details. Once I have discussed the highlights of each, I'll show you a specific example of how they are used in the actual download process.
For the container to display the status of a download through a UI of its choice, or to allow the user control over the download process, it will need to be included in the download process of large persistent property BLOBs. The control does this by asking the container to create a moniker for it through its implementation of IBindHost. The interface definition of IBindHost is as follows:
interface IBindHost : IUnknown
HRESULT ParseDisplayName([in] LPOLESTR pszName,
[out] IMoniker **ppmk);
HRESULT GetBindCtx([in] DWORD dwReserved,
[out] IBindCtx **ppbc);
If IBindHost is not implemented, then the control will have to create the moniker itself. The downside of this is that the browser is not able to participate in the management of this download process. For the user, there is no way, short of closing the browser down, to manage the download through a UI of some kind (a Cancel, Pause, or Resume button, for example).
The actual value of a data path property is the URL address of the data file it represents. This is stored as either a path relative to where its HTML page is stored on the server (such as ../myserver/myfile.mpg) or as an absolute path (such as http://www.mycompany.com/webpages/myserver/myfile.mpg). IBindHost::ParseDisplayName is provided by the container to return an absolute moniker from either a relative or absolute path name string passed into it by the control when it wants to bind to that data. If a relative location is passed in, this function transforms it into an absolute representation by appending it to the absolute location of its HTML page.
If the URL is absolute, and the container does not support IBindHost, the control can still find the BLOB. If the URL is relative, it requires IBindHost support to locate the data since the control has no idea what the URL is relative to. Therefore, a container that supports data paths must implement IBindHost through its client site object. It also creates a bind context for this moniker that the control can hook into via its IBindHost::GetBindCtx member when the control wants to bind to a moniker to asynchronously obtain its data.
Support from the control side for asynchronous file transfer is done by supporting a few additional interfaces. Note that in order to provide maximum flexibility, the control should be written to handle the degenerate cases where a browser does not provide the support previously described.
The IBindStatusCallback interface contains members relating to management of the download process. A bind context is created by the container for each binding (download) operation through which the control and container can register to receive download notifications. IBindStatusCallback::OnStartBinding allows the control to hook into this bind context. The OnProgress member allows the control to receive regular download notifications from the system as to how much data has been downloaded. Each time another group of bytes arrives, or transfer is complete, the OnDataAvailable member is called.
In order for a control to hook into a bind context, it must obtain an IBindHost interface pointer through the container's client site. The client site is passed to the control by the container through the container's IOleObject::SetClientSite member as part of initialization. IOleObject is a large interface with 21 member functions. If the control uses this interface only to get its client site, this adds a lot of extra code to the control. The control can take a lightweight approach and implement IObjectWithSite and use its two members (SetSite and GetSite) to obtain its client site pointer instead of IOleObject.
IPersistMoniker is another new Internet-related interface. It's derived from IPersistStream, so it has the ability to load and save itself.
interface IPersistMoniker : public IPersist
HRESULTInitNew([in] IMoniker *pmkStore,
[in] DWORD grfMode,
[in] IBindCtx* pBindCtx);
HRESULT Load([in] IMoniker *pmkStore,
[in] DWORD grfMode,
[in] IBindCtx *pBindCtx);
HRESULT Save([in] IMoniker *pmkStore,
[in] BOOL fRemember,
[in] IBindCtx *pBindCtx);
HRESULT SaveCompleted([in] IMoniker *pmkStore);
HRESULT GetCurMoniker([out] IMoniker **ppmkStore);
If a control supports this interface, a container will create a URL moniker out of the URL in the DATA attribute of the <INSERT> tag and pass it to IPersistMoniker::Load. By doing this, the container tells the control to load all of its non-BLOB persistent data any way it wants to. This is a key point to remember: BLOB properties are not affected by IPersistMoniker. Only the small persistent property data stream is relevant here. The control can still download property BLOBs asynchronously regardless of whether or not it supports this interface. (I will explain this in more detail later).
If a control does not use any data paths for large BLOB properties, it should support as many IPersist interfaces as applicable while still trying to limit their number (to keep code size small). It is a trade-off that you must make. This makes a control as flexible as possible during initialization within various containers. (See the IPersistMoniker sidebar for more details.)
IProvideClassInfo3 is yet another new interface. By a control exposing the new IProvideClassInfo3 interface, a container can call members of this interface to easily find out if the control exposes data path properties, then obtain pointers to them. This saves the container from having to painstakingly parse the type information to obtain this data. (The "IProvideClassInfo3" sidebar explores this interface in more depth.)
On Your Mark, Get Set... Are You Ready?
These new interfaces enhance the cooperation between a container and a control with respect to downloading of bits from the Web site. A container expects that, when its call to IPersist*::Load returns, all of the control's properties have been loaded. If a control contains data path properties, their asynchronous downloads will be initiated before the return of this call. If, immediately following the return from IPersist*::Load, the control is told to do something with the data (but the data is not done downloading), problems may occur. For example, if a control is downloading an MPEG file in the background and the user hits the Play button, the control may not have enough data to do it.
To further enrich communication and prevent situations like this, the control can make its readiness state known to the container. A readiness state is the control's ability, at that current point in the download process, to respond to user interaction. These states are listed in Figure 5.
There may be script code that accompanies the HTML page the control is on (assuming that the browser supports OLE Scripting). This code may be dependent upon the readiness state of the downloading control to enable/disable other dependent controls on the form. To handle this, a new property (ReadyState) and event (OnReadyStateChange) have been introduced. The control can convey its current readiness states to the container through one or both of these mechanisms. Specifically, if a control makes use of any of these state transitions (not all controls will), then it must fire an OnReadyStateChange event as it transitions between each applicable readiness state. Note that script code must be written explicitly to use this event or check this property to complete the notification process.
For methods that have dependencies on the downloading data, the control should return the new code E_PENDING if it's not ready to perform the method. Basically this says that there isn't enough data here yet, so try again later. Within a method, the control determines its current readiness state and returns E_PENDING if it does not have enough data to act. OnReadyStateChange and ReadyState provide container-side mechanisms that help prevent calling a method before the control is ready; E_PENDING is designed to handle situations where a method has already been invoked before the data is there. For example, suppose that our MPEG control has its Play method invoked through script code immediately following its instantiation. If its readiness state is not at least READYSTATE_INTERACTIVE, then no playback should occur; it will return E_PENDING. Ideally, the script code would be written to check the readiness state before calling Play, but it's foolish to assume that all code will do this. E_PENDING is a catchall to avoid undetermined behavior in cases like this.
During asynchronous binding, the control and the container can receive repeated progress updates from the moniker via its IBindStatusCallback::OnProgress function. Two of the parameters the moniker passes into this callback method are the amount of data transferred so far (ulProgress) and the total amount expected over all calls to this method (ulProgressMax). The control can use ulProgress, ulProgressMax, and other data (such as the data transfer rate and the size of the file) to calculate its readiness state.
For example, suppose the user's machine has a 14.4 modem and is downloading a Web page with an MPEG control on it. The readiness state computation for the control figures out that, for an MPEG file of this size, it has the entire first frame-which it can display as a static image-when the readiness-state calculation value yields 4 percent. At this point, it can enter the Loaded/Can Render state. It fires the OnReadyStateChange(READYSTATE_LOADED) event and sets its ReadyState property to the same value. At 65 percent, the control may have enough data to start playback. It fires OnReadyStateChange
(READYSTATE_INTERACTIVE). In deciding that 65 percent is the cutoff to enter Interactive state, the control must be sure that the data currently in the MPEG file on the user's local machine will not be accessed at a rate that will result in attempting to access bits that have not arrived yet.
When developing the Web page, the author may have decided to use a playback control that would allow the user to control the playback of the MPEG file. This control mimics the buttons on a cassette player with Start, Stop, Pause, Rewind, and Fast Forward. The author wrote script code that enables all the buttons except Fast Forward as soon as the control enters READYSTATE_INTERACTIVE (the MPEG is 65 percent loaded). Once all the data is available (READYSTATE_COMPLETE), the Fast Forward button is enabled.
The OnReadyStateChange Visual Basic Script event handler might look like the code in Figure 6. Note that I have placed enabling code within each block that tests for readiness levels. This is because when this Web page is viewed a second time, myfile.mpg may be cached on disk already and the moniker can locate it immediately. (For Internet Explorer, the cache is usually saved in the \Program Files\Plus!\Microsoft Internet\Cache directory.) Thus, the control may call OnProgress and OnDataAvailable only once, to notify the container that the binding is complete. The control will never need to fire the OnReadyStateChange event with the Interactive state-only the Complete state will be fired. If the Visual Basic Script code is not written to handle this, only the Fast Forward button may be enabled.
Some browsers may not support OLE scripting, so the event notifications never get channeled to the scripting code. If a control sets its ReadyState property to the appropriate value, the container can obtain the state of the control's readiness at any time and not depend on the control firing an event. Without scripting, the container may need the readiness state property for its own specific user interface. To be as flexible as possible, a control should therefore support both means of conveying its readiness state-ReadyState and OnReadyStateChange. (See the sidebar "The Readiness State" for additional comments.)
A Day in the Life of an Internet-Aware Control
Let's tie all this together with an example that starts with the Webmaster placing our MPEG control (mympg.ocx) onto a page and ends with the control being fully functional on the Web page displayed on the user's local machine. I'll assume that the browser used to view the Web page supports OLE controls, OLE scripting, and all of the interfaces needed for asynchronous binding.
Most of this example will be supported directly by authoring tools and browsers from the container side, and through control development tools like Visual C++/MFC and Visual Basic from the control side. It is more an in-depth explanation of what goes on behind the scenes to help you fully understand the interaction between container and control. The comforting news is that, as a developer using future versions of Visual C++/MFC or Visual Basic, you normally will not have to code much of this yourself.
I will use a hypothetical authoring tool for our example since OLE control support is being developed for authoring tools as we speak. The author chooses our control, possibly from a control palette similar to that used in Visual Basic. The author drops it on the Web page, and the authoring tool creates a design-time instance of it. Next, through the property page or the container's property browser window, the author sets the Title and Border properties. For the MPGfile property, the author clicks "É" to bring up the Load File dialog, and selects myfile.mpg.
Eventually, the author decides the page is complete and saves it. At this point, the authoring tool somehow lets the author specify whether the property data should be stored persistently into a stream file or as inline HTML data. The author decides to serialize the title ("My MPEG Title") and border style (some integer) to a stream on the server at the URL http://www.mycompany.com/webpages/mypage/mypropdata.stm. The authoring tool understands data path properties and, through IProvideClassInfo3, is able to tell from the control's type information that the MPGfile property is a data path property. It therefore saves the URL for myfile.mpg into the mypropdata.stm file as well. If the control provides both a relative and absolute MPEG file property (MPG_Path_Relative and MPG_Path_Absolute, for example), then the author should use the authoring tool to save both the absolute address (http://www.mycompany.com/webpages/mpgfiles/myfile.mpg) and the address relative to the HTML Web page (..\mpgfiles\myfile.mpg).
The authoring tool also saves the URLs of mympg.ocx and mypropdata.stm into mypage.html. They are saved under the <INSERT> tag using the CODE and DATA attributes, respectively. (There are other tags, but for simplicity's sake I'm not going to examine them. For a full definition of <INSERT>, see "Inserting Multimedia Objects into HTML3." To find it, point your browser at http://www.w3.org/pub/WWW/TR/ and look for the WD-insert-YYMMDD.html file. As this document is updated, its date is changed and is reflected in the YYMMDD part of the filename.) The resulting HTML code may look like this:
//Arbitrary name set by author to identify control to
//other controls (scripting, for instance)
//Allows browser to detect type of object so it won't
//waste time downloading it if not supported
//File containing the object's implementation of the
//URL of file that contains the CLSID and persistent
//data (excluding property BLOBs)
If the user decides that the actual Web locations where everything (HTML page, OCX file, persistent data, and MPEG file) was stored is fine for the ship version, then nothing more needs to be done. However, if the author decides to move the files or change the directory structure, the URL filenames must be changed. In particular, the INSERT tag must be updated with the new locations of mympg.ocx and mypropdata.stm. Within mypropdata.stm, the data path property for myfile.mpg may also have to be updated.
A user, through the Internet Explorer 3.0 browser (or any browser that supports data path properties and OLE controls), enters the URL for mypage.html. The browser downloads the stream of HTML commands and begins parsing them until it encounters the <INSERT> tag. From the TYPE attribute it sees that the author has inserted an OLE object. Since it knows it can support OLE objects, it uses the CODE attribute to locate the code that implements the object. It first checks to see if the control has been previously installed on the user's local machine, either through a previous download or an unrelated installation program.
If the OCX file is not on the machine, it must be downloaded. The browser will follow the appropriate download strategy for executable files like EXEs and OCXs (Internet Explorer 3.0 will use code signing). Once the download is complete, the browser calls the exported function DllRegisterServer to register the control with the user's local machine. Unless it is explicitly unregistered by the user, it is now part of the registered control store on that computer. The browser obtains the CLSID from its entry in the registry and passes this into CoCreateInstance to create the object, which returns the control's IUnknown pointer. (Note that <INSERT> also has a CLASSID attribute to get the CLSID directly.)
Non-BLOB Persistent Data
The browser next tries to pass the control its persistent data (excluding BLOBs) so it can initialize itself. How it does this depends upon two things: whether the data attribute contains a URL address to the data file on the server, or the data is actually embedded inline within the HTML code in base 64 encoded format.
If the data is embedded inline, the container calls QueryInterface for a persistent interface in the order of IPersistStreamInit, IPersistStream, IPersistMemory, IPersistStorage, and IPersistPropertyBag. (Note that it does not look for IPersistMoniker in this embedded case.) Depending on what it finds, the container puts the data in a stream for all but IPersistPropertyBag-where each property will be placed one at a time into an IPropertyBag object-and calls the Load method. The control reads the property values and initializes its corresponding non-BLOB properties. If any of these have to do with the UI, the control should invalidate its window to redraw itself before Load returns.
If the data attribute contains a URL address to the persistent property file (again, excluding BLOBs), the container calls CreateURLMoniker to create an (asynchronous) URL moniker for the data. It then calls IMoniker::BindToObject to download the data.
In its implementation of BindToObject, the URL moniker will QueryInterface the control for IPersistMoniker. If found, the moniker passes a pointer to itself to the control's IPersistMoniker::Load method. Using this moniker, the control calls IMoniker::BindToStorage to asynchronously download the non-BLOB property data itself. With IPersistMoniker, the control has complete control over the download and initialization of all of its persistent data. (For more on IPersistMoniker, see the "Details of IPersistMoniker" sidebar.)
If the control does not implement IPersistMoniker, the URL moniker asks the control for its persistent interfaces in the order of IPersistStreamInit, IPersistStream, IPersistStorage, IPersistFile, and IPersistMemory. This part is standard OLE stuff-you should implement whichever IPersist* interface you need. Once a persistent interface pointer is returned, the moniker will itself create a stream object and download the data into this stream by calling IMoniker::BindToStorage. Once the data arrives (which is usually very quick since this data should be small), it passes the stream to the Load method of one of these persistent interfaces.
BLOB Persistent Data
Recall that a data path property is itself a non-BLOB property that contains either an absolute or relative URL address to its BLOB-it is not the BLOB itself. When initializing its non-BLOB properties (and before returning from IPersist*::Load), the control will encounter any data path properties that it stored persistently. A URL moniker will need to be created from each URL address (one per data path property) to load the BLOB.
To include the container in the process, the control asks the container to create the moniker and the bind context it will use. If the container supports IBindHost, the control will pass the URL address to IBindHost::ParseDisplayName to get an absolute address. Only after it has an absolute address will the container pass the URL string to CreateURLMoniker. The system creates a URL moniker and passes it back to the container, which returns it to the control as an output parameter for ParseDisplayName.
Please note that, while asynchronous downloading of data path properties is one of the primary focal points of this article, it is not the only way to load data! In an Intranet (LAN) that uses the Web-browsing paradigm to browse an internal-only Web site, data can be transferred synchronously since network speed is not an issue. In this case the data comes in so fast that you might as well do it synchronously to get faster throughput. You can find out if a moniker is asynchronous by QueryInterface-ing it for IMonikerAsync. This "dummy" interface is just a way of finding out if the moniker is asynchronous. If QueryInterface returns NULL, you can call BindToStorage to load the data synchronously. Synchronous moniker binding is nothing new, so I will not discuss it here. (For an extensive explanation of this process, see Kraig Brockschmidt's Inside OLE, 2nd Edition, Microsoft Press.)
Once it determines that it has an asynchronous moniker, the control asks the browser for a bind context through IBindHost::GetBindCtx. This is how the container hooks into the binding to receive progress notifications. The container creates a bind context and registers its IBindStatusCallback within this context. The context is then passed back to the control as the output parameter of IBindHost::GetBindCtx. Once GetBindCtx returns, the control uses IEnumFORMATETC to register information about the type of data it wants. It also registers its own IBindStatusCallback within this same bind context to receive download notifications.
Using the IMoniker pointer that was returned to it by the container, the control calls IMoniker::BindToStorage, passing in the bind context. The control also passes in a pointer to an IStream, IStorage, or other medium that will receive the data, as appropriate for the format specified in the IEnumFORMATETC registered previously with the bind context. In synchronous binding, the stream/storage would contain the data when BindToStorage returns. With asynchronous bindings, the control should immediately release the storage pointer when BindToStorage returns. (This makes sense since the data will most likely not be there anyway, so the pointer is useless.) This permits the control to immediately return from the IPersist*::Load call and frees the container to begin other downloads.
As the data arrives, the system calls IBindStatusCallback::OnProgress for any object that has registered itself with the bind context for that binding. As discussed previously, our MPEG control could use information about the speed and size of the data being downloaded to calculate the percentage complete using the parameters sent into OnProgress. When it reaches the state that the download is not yet complete, but the user could begin to play the MPEG file, it can fire its OnReadyStateChange(READYSTATE_INTERACTIVE) event to the container. Since the container supports OLE Scripting, it knows how to route the event to the correct handler, which then enables the UI appropriately.
The main reason the control asks the container to create a moniker-instead of creating one itself-is so the container can become part of the download process. If the control is running in a browser that does not support IBindHost, the control must create the asynchronous moniker itself. If it wants to receive download notifications through its bind status callback, it must also create a bind context with which to register its IBindStatusCallback before binding to the moniker. The control creates the bind context by calling either CreateAsyncBindCtx or CreateBindCtx. If the control does not want to receive notifications, it can pass NULL into IMoniker::BindToStorage for the bind context argument.
It's less than ideal for the control to have to do all this work just because the container can't handle downloads properly. If the container does not support IBindHost, the control calls CreateURLMoniker and passes the URL value of its data path property. Ideally, both the relative and absolute paths should be made available by the control developer as different data path properties, such as MPG_Path_Relative and MPG_Path_Absolute. The author (and the authoring tool) should use both properties to store the absolute URL and the relative URL. If the control has only a relative URL to the BLOB and no container support, the control will probably not be able to locate the file.
As Internet apps become richer and more interactive, OLE Controls will take advantage of their success on the desktop and become a major contributor to active content on Web pages. Many controls today have small code sizes and no large persistent BLOB properties; these objects can be used today with little if any modification. Other controls, however, will need to be enhanced before they are useful on the net.
To reduce download time, you must keep your code size small. The new "OLE Control and Control Container Guidelines version 2.0" allows objects to implement only the interfaces they need to implement specific functions. Component categories allow containers to instantiate only those objects they can host properly. A minimal C++ base control class is included with the new Win32 SDK; you can use it to reduce code size, but it requires a good understanding of OLE. Visual C++ and Visual Basic will soon be enhanced to provide easier mechanisms for developing Internet controls.
Once you've reduced the size of your controls, you should use URL monikers to load large persistent property BLOBs asynchronously. You must be careful not to "paralyze" the user interface, so the user has to wait needlessly until all the data has arrived before doing anything useful. Cooperation between the container and the control during the download process allows the container to manage the transfer and to notify the user of progress. This added communication is handled by additional interfaces. If your controls need some enhancement, minimize download time by adding a few additional interfaces and shedding ones that are unnecessary. Once you do, your control will function as happily on the Internet as it does on the desktop.
Please see the Internet Development Web site (http://www.microsoft.com/intdev) for additional development tips, information, and pointers to specifications cited in this article.
Details of IPersistMoniker
Kraig Brockschmidt and Joshua Trupin
When dealing with asynchronous storage and URLs, the existing interface designed for file-based persistence (IPersistFile) is insufficient for use with URLs on the Internet. One problem is that IPersistFile is based inherently around UNC path names, requiring drive letters or server share prefixes and filenames. It also returns local file handles, which are meaningless when you're trying to fetch an Internet file. This just doesn't work well with off-site persistence mechanisms. The IPersistMoniker interface was designed to handle persistence for asynchronous monikers. It looks almost exactly like the IPersistFile you know and love, with member functions like InitNew, Load, and Save. Each of the data-related functions takes a moniker, an open mode (shared/exclusive/read-only), and a bind context like all moniker operations.
However, the bind context (a system object that implements IBindCtx), which was often seen as a mere curiosity, has been transformed into a vital piece of the equation. In the past, a program wishing to resolve a moniker would call CreateBindCtx to create a generic bind context, then pass it to the IMoniker::BindToObject method as a parameter. With an asynchronous moniker, a bind context is still created the same way, but now it's used as a location to register callback hooks for all interested parties. A new interface, IBindStatusCallback, can be created and registered with the bind context through the IBindCtx::RegisterObjectParam method. Through IBindStatusCallback, an asynchronous moniker sends notifications of download progress through a number of methods-whether the owner of that callback is the control, its container, or even the container's container. Now, when the data referenced by an IPersistMoniker is actually retrieved through IPersistMoniker::Load, everyone who has registered a status callback will receive status notifications. The absence of unnecessary handshaking between container and control makes this quite efficient (see Figure A).
A number of objects usually want some sort of progress report during an asynchronous transfer, and adding a bunch of connection points can get messy and inefficient quite quickly. Using the bind context, which was created with situations like this in mind, provides a much cleaner solution for this problem.
But back to IPersistMoniker. It isn't necessarily something that you'd want to use every time out, and it may not be appropriate for what you're trying to accomplish. How can you decide? Five reasons, in general, should tell you more about your particular requirements.
First, IPersistMoniker lets you provide complete asynchronous property retrieval (especially for inline BLOBs). Out of the three elements of a CLSID/properties/BLOBs transfer, you can read properties and BLOB data asynchronously with IPersistMoniker. However, there are some things you might not want to retrieve asynchronously. For instance, what happens if your control is asked to render itself and you've downloaded the foreground color property but not the background color property?
Another problem is that the CDK and SDK simply aren't set up very well to handle asynchronous properties. Properties are usually only a couple of hundred bytes. The benefits gained through this kind of threaded downloading don't make up for the potential problems you might encounter because of missing property values. If you really want the fine level of control you get by retrieving properties asynchronously, go ahead and do it. But IPersistMoniker is really designed for retrieval of inline BLOBs. It's best to write code to first read your properties quickly, then let the BLOBs trickle in asynchronously.
Second, IPersistMoniker gives you control over the exact call to IMoniker::BindToStorage. Therefore you can request any kind of storage pointer back from the downloaded data, be it IStream, IStorage, or any other custom interface.
Third, IPersistMoniker lets you specify the exact flags used to open the storage object. You might want to change the data reads from transactional to direct, or set the mode to read/write or deny read.
Fourth, you can use IPersistMoniker to install your own IBindStatusCallback and get the notifications you want for the asynchronous transfer of the control's properties.
Finally, IPersistMoniker lets a control intercept and filter calls to its container's IBindStatusCallback. If you want, you can take everyone else's callbacks out of the bind context, put yours in, then put your own filters over all the other ones. This would let you convert incoming data to another format before passing it to other objects' callbacks. This particular form of tampering is perfectly legal in the world of OLE controls.
Kraig Brockschmidt and Joshua Turpin
IProvideClassInfo3 is something of a sidelight, but it's an important way for authoring tools to get class information they need, instead of having to pick through a type library tediously, which is not much fun. A new interface, called IProvideClassInfo3, has been created to remedy this situation by letting a program get pieces of control typelib data very quickly.
interface IProvideClassInfo3 : IProvideClassInfo2
//Obtained from IProvideClassInfo2
//HRESULT GetGUID([in] DWORD dwGuidKind,
[out] GUID *pGUID);
HRESULT GetGUIDDwordArray([in] REFGUID rguidArray,
[in-out] CAUUID *pcaUUID,
[in-out] CADWORD *pcadw);
GetClassInfoLocale([out] ITypeInfo **ppITypeInfo,
[in] LCID lcid);
HRESULT GetFlags([in] REFGUID guidGroup,
[out] DWORD *pdwFlags);
IProvideClassInfo3 has a generic function GetGUIDDwordArray. When passed a GUID-based ARRAYID representing an array you want to use, IProvideClassInfo3::GetGUIDDwordArray returns an array of GUIDs, an array of DWORDs, or both. With data path properties, this is especially useful for retrieving an array of property DISPIDs and the array of GUID types that correspond to them. Thus the container can quickly obtain an exact list of properties and their types.
IProvideClassInfo3, if you haven't guessed, is a modification of IProvideClassInfo2 (which superseded IProvideClassInfo). IProvideClassInfo has only one member function, GetClassInfo, which returns an ITypeInfo pointer to the control's type information. It's a lot easier to do this than get it by walking through a typelib. However, IProvideClassInfo doesn't provide a way to specify a locale ID, so there's no way to specify the language in which the type info should be returned. IProvideClassInfo2, defined in the latest versions of OLECTL.H (Visual C++ 4.0 and higher), but not extensively documented, doesn't correct this shortcoming either. It does supply a second method, GetGUID, which takes a DWORD constant and returns the system GUID for that particular type. Only one constant, GUIDKIND_DEFAULT_
SOURCE_DISP_IID, was defined for GetGUID. This constant lets you get the default source interface of a controller (DISPINTERFACE), its CLSID, and that sort of thing. GetGUIDDwordArray takes the idea of GetGUID a step farther. It takes a GUID that stands for something like "give me a control's incoming interfaces" and puts them all in an array without the caller having to iterate through all properties. Valid ARRAYIDs are defined for use with IProvideClassInfo3::GetGUIDDwordArray (see Figure B). You can define more for your own use.
Another new method added to IProvideClassInfo3 is GetFlags. OLE compound documents already support miscellaneous status bits, but this data is limited to 32-bit flags. IProvideClassInfo3::GetFlags is a generic extension to this concept, provided to address extensibility issues. A fix for the GetClassInfo locale problems has been designed with the other new method, IProvideClassInfo3::GetClassInfoLocale. This method is an extension to IProvideClassInfo::GetClassInfo that takes an LCID so the caller can specify the proper locale where supported.
The Readiness State
When the idea of asynchronous property fetching is introduced, there's always a potential problem looming. What if a control has certain methods or properties that depend on the data being there? And what if the data's not there? Obviously, requests for these properties and calls to these methods can't be fulfilled. This is a new concept for OLE, which has always been a synchronous model by default-you call IPersist*::Load, and when the call returns, the control's been loaded and initialized completely.
But now the all-or-nothing concept of control loading has taken a hike. A new standard property, ReadyState, has been defined. ReadyState is a DWORD that represents one of five possible readiness states: Uninitialized, Loading, Loaded/Can Render, Interactive, or Complete.
Uninitialized is the state where the control is loaded, but is completely uninitialized-the container hasn't called IPersist*::Load (the container can't call anything else yet to ask for the property value). The container has to draw an image for this control on its own, since the control has no information at all.
Loading is the state of a control when it's loading its properties asynchronously after it has returned from IPersistMoniker::Load. In this state, not all properties are available. A container has to expect that many operations may return E_PENDING, including requests to render the control.
The Loaded/Can Render state means the control has finished loading its immediate properties, and has enough information to at least draw something via IViewObject2::Draw, even if it's just a simple box.
Interactive state is when the control can respond to user events like mouse clicks as well as fire events, be in-place activated, handle all property operations, and so on. The control can also provide its type information. However, some properties (like images) might still be coming in at higher resolutions. An analogous state is an image map on a Web page, where you can click on it before the bitmap is displayed in the window. When new visual data arrives, the control can fire off IAdviseSink::OnViewChange to tell the container it's ready to be redrawn.
Complete is the final state, where all the data has come down and everything's available, as if the binding has been performed synchronously.
The control isn't the only object that has to know how far along it is during initial setup. A container has to know that it's not always dealing with complete entities, so a new error code, E_PENDING, has been defined. It's the OLE equivalent of a Magic 8 Ball, saying "answer uncertain, ask again later." This is better than an undefined result, but it's advisable for a control to load the important stuff first to minimize the calls it can't fulfill.
To make things easier for a container, so it doesn't have to constantly poll a control for its ReadyState property, a standard event, OnReadyStateChange, has been defined for controls processing asynchronous downloads. This event is fired whenever a control changes its ReadyState. The ReadyState is generally needed by user-supplied code, so OnReadyStateChange is defined as a standard event instead of using the IPropertyNotifySink::
OnChanged notification. This makes the event visible in programming tools and allows the control to pass the new ReadyState value along with the event.
A brief example will demonstrate how this event might be used. Suppose you're creating a Visual Basic script to manage a Web page containing an MPEG control and a Play button. Initially, the Play button is disabled, since there's no data to play in the MPEG control. The MPEG control's documentation tells you that MpegControl.Play isn't available until MpegControl.ReadyState equals Complete. So the Visual Basic code can contain an event-handling function, MpegControl_OnReadyStateChange, which is called as the ReadyState changes. When the value passed in as a parameter equals Complete, the Visual Basic code can enable the Play button on the form. Of course, the MPEG control might also define its Interactive state to mean that it's able to fulfill the Play method, although the entire clip won't be able to play. That decision should be made by the designer on a control-by-control basis, and documented so a programmer can write appropriate code to use the controls.
Notice that the specification doesn't provide a standardized Progress event from the control to the container through the normal event mechanism. There was no agreement about whether this would deliver value to the developer, since it's easy enough for a container to implement an OnProgress event as part of its own extended events. You can get the same functionality without foisting more work upon the control. Once again, excess control-side requirements have been avoided, because packing them with lots of little goodies that the container can do just as easily would slow code downloads. As a rule, goodies should be implemented on the container side.
Kraig Brockschmidt and Joshua Trupin
From the April 1996 issue of Microsoft Systems Journal.