OLE Integration Technologies

Building on the Component Object Model

Kraig Brockschmidt

Kraig is the author of Inside OLE 2, published by Microsoft Press. Kraig can be contacted at Microsoft Corp., One Microsoft Way, Redmond, WA 98052-6399.


OLE 2, along with the technologies that fall under the OLE umbrella, is all about integration--integration between functional components of all sorts, wherever they may be. These components can be located in the system, inside applications, inside in-process DLLs or out-of-process EXEs, and, in the future, even in modules distributed across a network.

The basis for this integration is the Component Object Model (COM), described in the article, "The Component Object Model," by Sara Williams and Charlie Kindel on page 14 of this issue. OLE uses COM as the low-level plumbing that provides transparent communication between components through a binary standard (COM interfaces and structures). As COM evolves, OLE will automatically benefit and gain support for diverse system services, from database access, to messaging services, to system management, and more.

OLE's history is concerned primarily with the creation and management of compound documents, but now OLE is much more than that. OLE integrates components that can come in many shapes and sizes. The interfaces provided on those components can also vary widely. In some cases, a particular component is implemented by OLE itself (to form a standard on which applications can depend). Other components are implemented by various types of applications. Components that are primarily users of interfaces implemented on other components are called "clients" or "containers," depending on what they do with those interfaces. Modules that implement components with interfaces are called "servers." All components based on COM are called "Component Objects." For convenience, the use of the word "object" by itself in this article means "component object."

OLE is not an all-or-nothing technology. When using or implementing a component, you can use as little or as much of OLE as you find appropriate. This article examines the OLE technologies that build upon COM, as shown in Figure 1. You'll see that OLE is a very rich collection of features to integrate data storage and exchange, programmability, compound documents, and controls.

Structured Storage

Today's world of component integration requires the ability for many components to share a common, byte-oriented storage medium (whether a disk file or a record in a database). Each component needs storage in which to save its persistent state. OLE's Structured Storage is an abstraction layer for accomplishing this level of integration. Structured Storage can be built on top of any file or other storage system, as shown in Figure 2 for a hard-disk system.

In Structured Storage, any byte array can be structured into two types of elements: storages and streams. Each element has a name, which can be up to 31 Unicode characters in length. A storage implements the IStorage interface, which has directory-type functions for creating, destroying, moving, renaming, or copying other elements. A stream implements the IStream interface, with functions analogous to traditional file I/O (such as read, write, seek, and so on). In fact, IStream members have a direct one-to-one correspondence with the file I/O functions in the Win32 API and in the ANSI C run-time library. Example 1 illustrates Win32 and IStream similarities.

A storage can contain any number of other storages and streams within it, just like a directory can contain files and subdirectories. However, a storage isn't restricted only to being disk-based. Structured storage can be implemented on top of any byte-oriented storage system (or byte array), such as a disk file, a block of memory, a database field, and the like. Regardless of the medium, however, structured storage provides uniform access through the standard IStorage and IStream interfaces. The storage model also defines transactioning for these elements, where you can create or open an element in "transacted" mode (in which changes are not permanent until committed) or "direct mode" (where changes are immediately permanent).

In effect, this "file system within a file" leaves the exact layout of the file to the system, but makes incremental access to elements the default mode of operation. With such a system, the application can effortlessly give individual streams (or even entire storages) to other components in which those components can save whatever persistent information they desire.

This system is perfect for creating compound documents, and the technology known as "OLE Documents" relies on this structured storage facility. To further facilitate the creation and sharing of files (even across platforms), OLE provides a standard implementation of disk-based storages and streams in what are called "Compound Files." This facility is compatible with future versions of Windows (the code is being written by the same developers at Microsoft). Furthermore, Microsoft licenses the Compound File source code to other vendors for use on other operating systems.

A major benefit of having a single, standard implementation of structured storage on a given platform is that any application (including the system shell) can open and navigate through anyone's compound file. Elements of data are no longer hidden inside a proprietary file format. You can freely browse the hierarchy of storage and stream elements. With additional naming standards and standardized stream formats for specific information, every application can retrieve significant information from any given file without having to load the application that created that file.

Microsoft Windows95 will exploit this browsing ability by offering shell-level document searching. Windows 95 looks in compound files for a stream called "Summary Information," in which applications store data such as creation and modification times, author, title, subject, revision number, keywords, and so on. Windows 95 can match this information against a user query. What was once a feature buried inside applications for one document type is now a standard part of the system itself for all documents.

Object Persistence

Structured storage is necessary to allow multiple components to share the same disk file or other storage. A component indicates its ability to save its persistent state to a storage or stream by implementing the interface IPersistStorage or IPersistStream, respectively. (There is also an IPersistFile interface for components that save to separate files.)

The container application that manages such persistent objects creates the instances of IStorage or IStream to give to components that implement IPersistStorage and IPersistStream. The container tells components to save or load their persistent states from the storages or streams. Thus, the container remains in control of the overall document or file, but gives each component individual control over a storage or stream within that file. This tends to make structures within a file more intelligent--that is, placing more of the code knowing how to handle the structures into components rather than in the container.

Example 2 shows how a container would open an IStorage and have a component save into it through IPersistStorage. If the component doesn't support IPersistStorage, then the container cannot possibly try to save the component that way. This shows the power of the QueryInterface function and the interface-oriented architecture of OLE. You can't ask a component to do an operation it doesn't support.

Persistent, Intelligent Names (Monikers)

Think for a moment about a conventional filename that refers to data stored somewhere on disk. The filename essentially describes the "somewhere," and so the name identifies a file that could be called an "object" (in a primeval sort of way). However, this is limited, because filenames have no intelligence. All knowledge about how the name is used exists elsewhere, in whatever application uses that filename.

Now think about a name that describes the result of a database query, or a range of spreadsheet cells, or a paragraph in a document. Then think about a name to identify a piece of code that executes some operation on a network server. Each different name, if unintelligent, would require every application to redundantly understand the use of that name. In a component-integration system, this is far too expensive. To solve the problem, OLE has "persistent, intelligent names," otherwise known as "monikers."

A moniker is a component that encapsulates a type of name and the intelligence to work with that name behind an interface called IMoniker. Thus, users of the moniker pass control to the moniker whenever they want to work with the name. While IMoniker defines the standard operations you can perform with a moniker, each different moniker class defines what data makes up the name and how that name is used in binding. A moniker also knows how to serialize itself to a stream, because IMoniker is derived from IPersistStream.

The most basic operation in the IMoniker interface is that of binding to the object. IMoniker::BindToObject runs whatever algorithm is necessary in order to locate the object of reference and returns an interface pointer to the component that works with that information (this pointer is unrelated to the moniker itself). Once a client has bound to the referenced object, the moniker falls out of the picture entirely.

Types of Monikers

OLE defines and implements five basic type of monikers: file, item, generic composite, anti, and pointer. A file moniker maintains a text filename persistently and the binding means to locate a suitable application and have it load the file (returning an interface pointer to the "file" object). Item monikers are used in conjunction with file monikers to describe a specific part of a file that can be treated as a separate "item" object. To put a file and item moniker together requires the generic composite moniker. This type exists only to contain other monikers (including other composites), and its persistent data is just the persistent data of all the contained monikers in series (separated by a delimiter). Binding a generic composite means binding those it contains in turn.

A composite moniker is used whenever you cannot create a reference that is described by a single, simple moniker. A range of cells in a sheet of a Microsoft Excel workbook requires a file moniker to identify the workbook, an item to identify the sheet, and an item to identify the range in the sheet. Such a composite moniker is shown in Figure 3. Code that would create this moniker is shown in Example 3.

The antimoniker and pointer moniker are special types. An antimoniker annihilates the last moniker in the series in a composite. A pointer moniker wraps an interface pointer into a moniker where binding is nothing more than a QueryInterface call. These are provided for uniformity, and neither supports persistence.

Of course, if OLE's standard monikers are not suitable for your naming purposes, you can always implement your own component with IMoniker. Since you encapsulate your functionality behind the interface, your moniker is immediately usable in any other application that knows how to work with IMoniker.

Working with monikers is generally called "linking," the moniker's information being the "link" to some other data. OLE uses monikers to implement linked compound-document objects. This involves other user-interface standards for managing links. OLE also implements a central "running object table" in which monikers for already-running objects are stored. This prevents excess processing when a file is already loaded or when other data is already available in some other application.

Uniform Data Transfer and Drag-and-Drop

Structured storage and monikers integrate storage and naming functions. Once you have found a component that can read from storage, you'd normally like to have it render data for you. OLE's Uniform Data Transfer mechanism is the technology for data transfers and notifications of data changes between some source (called the "data object") and something that uses the data (called the "consumer"). All of this happens through the IDataObject interface implemented by the data object. IDataObject includes functions to get and set data, query and enumerate available formats, and establish a notification loop with the data source.

The "uniform" aspect arises from the fact that IDataObject separates exchange operations (get, set, and so on) from specific transfer protocols like that of the clipboard. Thus, a data source implements one data object and uses it in any OLE transfer protocol: clipboard, drag-and-drop, or compound documents. The OLE protocols (unlike the existing Windows protocols) are only concerned with getting an IDataObject pointer from the source to the consumer. Once transferred, the protocol disappears and the consumer just has to deal with a uniform IDataObject. Source and consumers can thus implement a core set of functions based on IDataObject and build little protocol handlers on top of that code.

Data Formats and Transfer Mediums

Besides the separation of transfer from protocol, OLE also makes data transfer much more powerful and flexible with two data structures: FORMATETC and STGMEDIUM. FORMATETC improves on the clipboard format of Windows--hence its name ("Format_"). The Windows clipboard format only describes the layout of a data structure (for example, CF_TEXT describes a null-terminated ANSI character string). FORMATETC adds a detail field (full content, thumbnail sketch, and so on), a device description (the device for which the data is rendered), and a transfer-medium identifier.

This last field brings us to STGMEDIUM, an improvement over the global memory handles. Existing Windows protocols only allow data exchange via global memory, which can be inefficient for large data. STGMEDIUM allows you to reference data stored in either global memory or in another medium--which could be a disk file, an IStorage, or an IStream.

Together, FORMATETC and STGMEDIUM allow you to keep data stored in the most appropriate (and efficient) medium and still ship it off to other applications. This can result in significant performance gains for applications that would otherwise load large data sets into global memory, only to have these swapped out to disk again by the virtual-memory system.

Clipboard and Drag-and-Drop

Other OLE technologies build upon the uniform data-transfer concept so you can take advantage of the improvements, regardless of how you transfer data.

First, the OLE DLLs provide functions to work with the system clipboard through IDataObject. A source cuts or copies data by packaging it into a data object and handing an IDataObject pointer to OLE's OleSetClipboard function. OLE, in turn, makes the formats therein available to all other applications (non-OLE applications can only see global-memory-based formats). When a consumer of data wants to paste from the clipboard, it calls OleGetClipboard to obtain an IDataObject representing the clipboard contents. Through IDataObject, the consumer checks formats or requests renderings. Data placed on the clipboard by non-OLE applications are completely available through this interface. So, you can toss out your old clipboard code and switch easily to the more powerful OLE mechanism.

Another technology that builds on data transfer is OLE's Drag-and-Drop feature, really nothing more than a slick way to get an IDataObject pointer from a source to a consumer, or "target." The source decides what starts a drag-and-drop operation (usually a mouse click-plus-move in a specific place). It then packages up its data into a data object--exactly as it does for the clipboard--and calls OLE's DoDragDrop, passing a pointer to its implementation of the IDropSource interface. Through this interface, the source controls the mouse cursor and drop or cancellation times.

The target, on the other hand, implements the interface IDropTarget and registers it with OLE for a specific window. When the mouse moves over that window, OLE calls functions in that IDropTarget according to what is happening with the mouse: enter window, move in window, leave window, or drop in window.

In these functions, the target indicates the effect of a drop at the mouse location point, modified by the Ctrl and Shift keys. Valid effects are a move (no keys), copy (Ctrl), link (Shift+Ctrl), or "no-drop." These are specified using DROPEFFECT_* flags. The effect is handed back to the source to indicate which cursor to show; see Figure 4. These default cursors are handled by OLE itself, leaving little for the source to do, as shown in the typical implementation of IDropSource (excluding IUnknown functions) in Example 4. Sources do have the ultimate say as to which cursor is shown, of course.

Note that DoDragDrop, in addition to watching mouse motion and the Ctrl/Shift keys, also watches the Esc key (used to cancel the operation) and the mouse button for an "up" message (used to cause a drop), as illustrated in Figure 5.

When a drop occurs on a target, that target just ends up with the source's IDataObject pointer--exactly as it would after a call to OleGetClipboard. At this point, the transfer protocol again disappears, and the consumer only must deal with IDataObject. The same is true for the source, which packages data into a data object for clipboard or drag-and-drop. Because drag-and-drop works equally well within and between applications, you get considerable mileage from one piece of code. The icing on the cake is that by adding a few formats for compound-document objects, you can suddenly start exchanging compound-document objects and controls by using the same protocols and the same code!

Notification

Consumers of data from an external source might want to know (asynchronously) when data in that source changes. OLE handles notifications of this kind through a component called an "advise sink" that implements an interface called IAdviseSink. This "sink" absorbs asynchronous notifications from a data source and can receive a new copy of the data if desired. The consumer that implements the advise sink connects it to the source's IDataObject through a member function called DAdvise. Disconnection happens through DUnadvise. In making the connection, the consumer indicates whether it would like a fresh data rendering. When the data object detects a change, it then calls IAdviseSink::OnDataChange to notify the consumer, as shown in Figure 6.

The IAdviseSink interface contains additional member functions that are used with other interfaces (such as IViewObject for notifications when a component's display image changes and IOleObject for state changes in compound-document objects). However, it's not designed to handle arbitrary notifications from arbitrary components. Such a task requires "events," which are introduced with OLE Controls (but which are more fundamental than controls, of course). OLE Controls are discussed a bit later.

OLE Automation

Another key aspect of integrating components is the ability to drive them programmatically--that is, to control them without requiring an end user's presence. This means having components expose their end-user functionality (for example, menu commands and dialog-box interaction), as well as properties by way of interfaces so that a scripting tool can be used to invoke that functionality.

There are two sides to this picture. On the one hand are components that are programmable by way of interfaces, or "automation objects." On the other hand is an application that provides a programming environment in which a developer or advanced user can write scripts or create applications that use other automation objects. These are called "automation controllers." To make all this happen, objects need a way to programmatically publish their interfaces (the method names and parameter types, as well as object properties) at run time such that the controller can perform type-checking and present lists of callable functions to the programmer.

The technology that supports this is OLE Automation, primarily through an interface called IDispatch. Applications that expose functions and properties for various application objects (the window frame, document windows, parts of the document, and so on) do so by implementing IDispatch on each of those components. However, IDispatch has only a fixed set of member functions. How, then, does each component supply its unique features?

The answer is an OLE Automation entity called the "dispinterface"--an implementation of IDispatch that responds to a specific set of custom methods and properties. An application frame and a document would both implement IDispatch, but would have different dispinterfaces.

Dispinterface uses the function IDispatch::Invoke, the prototype for which is shown in Example 5. The dispID "dispatch identifier" parameter tells Invoke which method is being called, or which property of this object is being get or set. The wFlags parameter indicates whether this call to Invoke is a method call or a property get or set operation. An object's dispinterface, then, is primarily the set of dispIDs to which the object will respond through Invoke, and this varies from object to object, of course. Since some methods take parameters, and properties have types associated with them, the dispinterface also includes all of this "type information." Other functions in IDispatch make the type information available to automation controllers, so those controllers can use the types to enhance their programming environments.

When creating an automation object, you create a file using the Object Definition Language (ODL) to define a dispinterface. This file is then run through a special compiler that generates a "type library" containing all the type information for any number of automation objects and dispinterfaces. This type library, which can be kept in a separate file or attached to a server module (DLL or EXE) as a resource, provides a way for automation controllers to discover what automation objects and dispinterfaces are available, without actually having to instantiate components just to ask for the information through IDispatch.

The type library itself is a component that implements the interfaces ITypeLib, ITypeInfo, and ITypeComp. You generally don't have to implement these interfaces. OLE provides the implementations that work on any underlying type library. Automation controllers use these interfaces to navigate through all the information in the library so as to present the programmer with lists of callable functions on an object, to extract parameter types to perform checking, and so forth.

Visual Basic (VB) and the related dialect Visual Basic for Applications are both automation controllers. When you run a piece of VB code such as that shown in Example 6, VB will translate the method calls and property manipulations in the VB code that uses the dot operator into IDispatch::Invoke calls to the component in question. Ultimately, all the calls are being made through the binary standard of COM interfaces, so VB doesn't care what language was used to implement the automation object.

This illustrates the integration power of automation. VB can create and manage many automation objects from many different applications at once, and use them to programmatically combine information from a variety of sources. Automation is exceptionally powerful for developers who are using off-the-shelf applications such as Microsoft Word or Shapeware Visio to create custom solutions. Adding automation support to an application opens up that application to a tremendous number of new uses. In addition, developers can encapsulate business logic into components, and make this functionality accessible through high-level, third-party tools, including fourth-generation programming languages and even productivity-application macro languages.

OLE Documents

Built on top of Structured Storage, Uniform Data Transfer, and Monikers, is the technology known as "OLE Documents." This technology supports the creation and management of compound documents. Two types of components are at work here. The container is the component that controls the document and manages relationships between the pieces of information in that document (such as layout). Compound-document objects are pieces that make up the content put into that document, and these pieces are supplied by servers (DLLs or EXEs).

OLE Documents is thus a way to integrate containers and servers through compound-document objects. The objects themselves can be shared in two ways. The first is embedding, where the entire object is "embedded" within the container--that is, the object's persistent state is kept in the document itself. Embedded objects always implement the IPersistStorage interface for this purpose, and containers that support embedding typically use a compound file to provide IStorage instances to embedded objects (but they don't have to).

The other way to share an object is linking, in which a graphic image of the object is cached in the container document along with a moniker that refers to the location of the object's actual data. The object's persistent state exists elsewhere, and the moniker provides the link to that data. Since a moniker can be as complex as desired, the path from the compound document to the source of the link can be very complex. Therefore, a document can contain linked objects to things as simple as a file, or as complex as a cell in a table in a document that is embedded within an e-mail message that exists in a particular field of a database on a particular network server. Monikers impose no limits.

Embedding is normally optimal for objects with small data sets, while linking is more efficient for large data sets (especially ones that are shared between multiple users on a network). Each link is a reference to a single source. By contrast, embedding the data means making a copy.

Compound-Document Objects

Compound document objects are nothing more than regular OLE objects that have a particular combination of interfaces. This is shown in Figure 7, along with the interfaces a container exposes to its objects. Note that the object interfaces shown in the figure are those seen by the container. Those in parentheses are implemented only by objects in DLLs. Those in EXEs implement only the unmarked interfaces and rely on DLL "object handlers" for the others.

The most important interfaces are IPersistStorage, IDataObject, IViewObject2, and IOleObject. The first two interfaces mean that compound-document objects support persistence to IStorage elements and that they support exchange of their data--primarily bitmap and metafile renderings of their display images that can be cached and displayed in the document. Caching allows the container to open a document for viewing or printing even when the code to handle the object is unavailable--the cached images are suitable for these purposes.

If an object implements the IViewObject2 interface, it has the ability to render itself directly to an hDC, usually the screen DC of the container's display or a printer DC on which the document is being printed. This gives control over rendering quality to the object itself. This interface is not limited to compound documents. Any object can implement it and possess this ability.

IOleObject is the primary (and rather sizable) interface that says, "This object supports the OLE Documents standard for compound documents." A container uses this interface for many purposes, the most important of which is activation. Activation means instructing the object to perform some action, called a "verb." The container will, as part of its user interface, show these verbs to the end user and forward them to the object when the user selects them. The object has full control over what verbs it wants to expose. Many objects have an "Edit" verb, which means "display a window in which this object's data can be modified." Others, like a sound and a video clip, have a "Play" verb, which means "play the sound" or "run the video." While the object defines what verbs it supports and what those verbs mean, the container is responsible for making the commands available to the end user and invoking them when necessary.

On the other side of the fence, the container must provide a "site" object for each embedded or linked object in the container. The site implements the interfaces IOleClientSite (which provides container information to the object) and IAdviseSink (which notifies the container when changes occur).

In-Place Activation

In cases other than playing a sound or a video clip, activation of an object generally requires that the object display another window in which the operation takes place (such as editing). For example, if you have a table from a spreadsheet embedded within a document, and you would like to edit that table, you would need to get the table back into the spreadsheet application to make changes. Right?

Not necessarily. OLE Documents includes In-Place Activation, also known as Visual Editing. This is a set of interfaces and negotiation protocols through which the container and the object merge their user-interface elements into the container's window space. In-place activation allows the object to bring its editing tools to the container, instead of taking the object to the editing tools. This includes menus, toolbars, and small child windows that are all placed within the container.

A number of interfaces are required to make all this work, on both the container and compound-document object. The interface names all start with the prefix IOleInPlace. By way of these interfaces, the two sides can create a mixed menu (composed of pop-up menus from both container and object), share keyboard accelerators, and negotiate the space around the container's frame and document windows in which the object would like to display toolbars and the like.

Because in-place activation is handled solely through additional interfaces for both container and object, support for it is entirely optional (but encouraged, of course). If a fully in-place-capable container meets an in-place-capable embedded object, then they achieve a high level of integration. If either side doesn't support the technology, however, then they can still work together by using the lower-level activation model that requires a separate window. Even when in-place is supported all around, the user can still decide to work in a separate window if desired.

In-place activation is not limited to just activating one object at a time, or activating objects only on user command. Objects can mark themselves to be in-place activated, without the mixed menu or toolbar negotiation, whenever visible. This means that each object can have an editing window in its space in the container. These objects respond immediately to mouse clicks and the like because their windows are in the container, and those windows receive the mouse messages. Only one object, however, can be "UI Active," which means that its menus and toolbars are also available. Of course, the UI Active object switches (and the user interface changes correspondingly) as the user moves between objects.

With multiple objects active within a document, you can imagine how useful it would be if some of those objects were buttons or list boxes. You could create forms with such objects, and create an arbitrary container that could hold objects from any source and benefit from all the other integration features of OLE. This is the reason for OLE Controls.

OLE Controls

An OLE Control is a compound-document object extended with Automation to support properties and methods through IDispatch. It relies on a mechanism called an "event," a notification that is fired whenever something happens to the controls (such as a state change or user input). A control transforms different types of external events such as mouse clicks and keystrokes (or application-specific events like the pickle vat on the factory floor springing a leak) into meaningful programmatic events. When these programmatic events occur, an event handler can execute code--such as showing a button pressdown, transmitting a character over a modem, or calling the pickle-vat repair company.

For the most part, OLE Controls is a set of extensions to the other OLE technologies such as Structured Storage (by way of adding an IPersistStreamInit interface) and automation (adding new ODL attributes for dispinterfaces, methods, and properties). OLE Controls defines a generic notification mechanism called "Connectable Objects" that is used to connect some sink object to a source, where the source wishes to call the functions of a certain interface implemented on the sink. This is like the IAdviseSink interface working with IDataObject, but more generic. This mechanism is used to implement events are actually meaningful and useful outside of controls. An object expresses the events it can fire as a dispinterface, which the event handler (such as a container application) implements with an IDispatch and connects to the object using the connectable objects technology. A similar extension involves property-change notification, which applies very well to controls, but is useful for any object that has properties of any kind to notify a sink when those properties change.

OLE Controls also introduces a technology called "Property Pages." This is a flexible user-interface model that any object can use to allow an end user to directly modify its properties. A property page is easily integrated into a tabbed dialog box, along with property pages from other objects as well, to create a consistent, easy-to-use environment for manipulating such data.

The new interfaces involved for connectable objects, property pages, property-change notification, and events make up the bulk of the additions to an OLE control over a regular in-place compound-document object. So, what actually is specific to controls? Not a lot, but a few key enhancements to the OLE Documents technology (through the interfaces IOleControl and IOleControlSite) comprise the final difference between a compound-document object and a control. For example, in compound documents, only the UI Active object can trap keyboard messages. By contrast, any control in a form or document should be able to respond to keystrokes at any time, so OLE Controls provides the mechanism to make it work. OLE Controls also defines mechanisms for handling special controls like labels, pushbuttons (where one can be the "default"), and exclusive button sets. In addition, the container application that manages the controls exposes a set of "ambient properties" (through a dispinterface) to all the controls to provide general defaults (such as colors and fonts). These additions, combined with property pages, property-change notification, events, and automation enhancements, make up "OLE Controls."

Since being a control or a container for controls is primarily being a compound-document object or container, OLE Controls leverages any work you do to support OLE Documents. Furthermore, applications such as Microsoft Access and Visual Basic support OLE Controls in form creation. With a few good controls, you can quickly create powerful front ends or custom business solutions with a minimal amount of code--all you have to do is add some VB code to the event handlers that these applications supply. As happened with VBX controls, you can expect the market to provide numerous, useful third-party OLE controls.

Conclusion

OLE is about integration on many levels. Components can come in various forms, be they simple functional objects with a straightforward interface (such as a string object), automation objects, data sources, compound-document objects, or controls.

Implementing a simple OLE object is very easy (as it should be). Implementing support for more complex technologies like compound documents and controls is a little more complex, but more help (in the form of books, articles, and tools) is available to developers as each month passes.

For example, Visual C++ 1.5 supports OLE by way of the Microsoft Foundation Classes (MFC), which greatly simplifies adding OLE automation, drag-and-drop, and compound documents to your app. Visual C++ 2.0 adds support for OLE Controls.

Regardless of your tools, if integration is what you seek, OLE is an answer. OLE helps you integrate components with many features and capabilities, allowing the features of those components to evolve over time. OLE is a complete solution to integration that will grow over time, to support distributed objects, for example, without requiring changes to existing code. Together, the integration technologies in OLE may make the dream of true component software become a reality.

Figure 1 All OLE technologies build upon COM and one another. Figure 2 Structured Storage sits above a file as a file system sits above a disk volume.

Example 1: Comparison of code to read a structure from: (a) a Win32 file; and (b) a stream.

(a) BOOL MyObject::ReadFromFile
      (LPSTR pszFile)
      {
      OFSTRUCT     of;
      HFILE        hFile;
      UINT         cb=-1;
      if (NULL==pszFile)
          returnFALSE;

      hFile=OpenFile(pszFile, &of,OF_READ);
      if (HFILE_ERROR==hFile)
          return FALSE;

      cb=_lread(hFile, (LPSTR)&m_data, sizeof(MYDATA));
      _lclose(hFile);
      return (SIZEOF(MYDATA)==cb);
    }

(b) BOOL MyObject::ReadFromStorage
      (LPSTORAGE pIStorage)
      {
      HRESULT        hr;
      IStream       *pIStream;
      LARGE_INTEGER  li;
    
      if (NULL==pIStorage)
          return FALSE;

      hr=pIStorage->OpenStream("MyStruct", 0
          ,STGM_DIRECT | STGM_READ
          | STGM_SHARE_EXCLUSIVE, 0, &pIStream);

      if (FAILED(hr))
          return FALSE;

      hr=pIStream->Read((LPVOID)&m_data
          , sizeof(MYSTRUCT), NULL);
      pIStream->Release();
      return (SUCCEEDED(hr));
      }

Example 2: Saving persistent data through IPersistStorage.

BOOL SaveObject(IStorage *pIStorage, IUnknown *pObject)
    {
    IPersistStorage *pIPS;
    HRESULT          hr;
    hr=pObject->QueryInterface(IID_IPersistStorage, (void **)&pIPS);
    if (SUCCEEDED(hr))
        {
        hr=pIPS->Save(pIStorage);
        pIPS->SaveCompleted(NULL);
    }
    return SUCCEEDED(hr);
    }

Figure 3 A sample composite moniker with a file and two item monikers to identify a range of cells in a particular sheet of a spreadsheet file.

Example 3: Code that creates a composite moniker with a file and two items.

IMoniker * MakeMonikerToRange(char *pszFile, char *pszSheet
    , char *pszRange)
    {
    IMoniker *pmkComp, *pmkFile, *pmkSheet, *pmkRange;    pmkComp=NULL;
    //"!" is a delimeter between monikers
    if (SUCCEEDED(CreateItemMoniker("!", pszRange, &pmkRange)))
        {
        if (SUCCEEDED(CreateItemMoniker("!", pszSheet, &pmkSheet)))
            {
            if (SUCCEEDED(CreateFileMoniker("!", pszFile, &pmkFile)))
                {
                //This creates a File!Item(Sheet) composite
                if (SUCCEEDED(CreateGenericComposite(pmkFile, pmkSheet
                    , &pmkComp)))
                    {
                    //Tack on the range to the File!Item(Sheet)
                    if (FAILED(pmkComp->ComposeWith(pmkRange, FALSE)))
                        {
                        pmkComp->Release();
                        pmkComp=NULL;
                        }
                    }
                pmkFile->Release();
                }
            pmkSheet->Release();
            }
        pmkRange->Release();
        }
    return pmkComp;
    }

Figure 4 Cursors used in Drag-and-Drop.

Example 4: The usual implementation of an IDropSource interface.

STDMETHODIMP CDropSource::QueryContinueDrag(BOOL fEsc, DWORD grfKeyState)
    {
    if (fEsc)
        return ResultFromScode(DRAGDROP_S_CANCEL);
    if (!(grfKeyState & MK_LBUTTON))
        return ResultFromScode(DRAGDROP_S_DROP);
    return NOERROR;
    }
STDMETHODIMP CDropSource::GiveFeedback(DWORD dwEffect)
    {
    return ResultFromScode(DRAGDROP_S_USEDEFAULTCURSORS);
    }

Figure 5 The DoDragDrop function enters a message loop that watches the mouse and keyboard and calls the IDropSource and IDropTarget functions. Figure 6 A source notifies a consumer of data changes through IAdviseSink.

Example 5: Signature of the IDispatch::Invoke function.

interface IDispatch : public IUnknown
    {
    ...
    virtual HRESULT Invoke(DISPID dispID, REFIID riid, LCID lcid,
         WORD wFlags, DISPPARAMS *pdispparams,  VARIANT *pvarResult,
         EXCEPINFO *pexcepinfo, UINT *puArgErr)=0;
    }

Example 6: Visual Basic code that translates to IDispatch calls.

Sub Form_Load ()
    Dim Cube as Object
    Set Cube = CreateObject("CubeDraw.Object")   'Creates the object
    'Each line of code here calls IDispatch::Invoke with different flags
    x = Cube.Theta                        'Property Get on "Theta"
    Cube.Declination=.0522                 'Property Set on "Declination"
    Cube.Draw                             'Method call
End

Figure 7 The interfaces of a compound-document object and container.


Copyright © 1994, Dr. Dobb's Journal