Developer.com Click here to support our advertisers
Click here to support our advertisers
SOFTWARE
FOR SALE
BOOKS
FOR SALE
SEARCH CENTRAL
* JOB BANK
* CLASSIFIED ADS
* DIRECTORIES
* REFERENCE
Online Library Reports
* TRAINING CENTER
* JOURNAL
* NEWS CENTRAL
* DOWNLOADS
* DISCUSSIONS
* CALENDAR
* ABOUT US
----- Journal:

Get the weekly email highlights from the most popular online Journal for developers!
Current issue -----
developer.com
developerdirect.com
htmlgoodies.com
javagoodies.com
jars.com
intranetjournal.com
javascripts.com

REFERENCE

All Categories : ActiveX

Chapter 3

Inside ActiveX


CONTENTS

This chapter provides an overview of the underlying technology of the ActiveX SDK. This includes the Component Object Model (COM) and the OLE technologies that build on the COM architecture, namely, Persistent Storage, Persistent Names, and Uniform Data Transfer. Although you can use ActiveX technologies without having to know about the Component Object Model, we thinks it's important to introduce these concepts. COM is the foundation of much of what makes up ActiveX, and of current and future Windows application development, as well.

The Role of the Component Object Model

The Component Object Model (COM) was introduced in Chapter 1as the architecture that underlies ActiveX and future Windows development. COM is not an initiative that suddenly "appeared out of nowhere" while Microsoft scrambled to reorganize for the World Wide Web. Nor is COM just a marketing tactic to give the end-user community incentive to buy a product. Not at all-COM has been in development since the early 1990s in response to numerous problems and difficulties with the existing Microsoft and non-Microsoft operating system and application architectures. These large-scale issues have affected the extensive installed base of Microsoft's commercial and personal end-users, as well as software developers.

Problems Targeted by COM

The computer industry has seen exponential advances in hardware technology and accompanying advances in software technology. As the industry advances and technology grows increasingly complex, customers demand more, and new problems emerge continually. Operating systems need more and more computer resources (CPU, RAM, disk) in order to run properly, and application software packages compete for the same resources at run-time.

What are the technological issues for end users? Most importantly, end users demand efficiency and robustness from software products. Anything less is unacceptable. To provide a suitable level of quality assurance to a software product, the vendor must ensure that several things hold true:

  • The application must be integratable. Applications must have the means to "talk to each other," for example, by using a common Application Program Interface (API), so that one application's output can be another's input.
  • The application must be able to share data. Well-behaved programs do not encode data in a proprietary format and do not prohibit their data from being read by other applications while it is active in memory.
  • The upgrade path must be smooth and painless. When a software package becomes obsolete or unwanted, the end user must have a simple way to do a version upgrade or a systemwide deletion.

In addition to the above issues common to both business and personal end-users, the following concerns are particularly relevant to the business user:

  • Access to legacy data. In addition to accessing data across PC platforms, businesses must be able to get to data-typically across a network- on computers that are often heterogeneous. For example, Microsoft Windows desktop machines hooked together into Novell LANs must be able to access UNIX Sybase database servers.
  • Networked application sharing. Unlike the solitary end-user, businesses often share software from network file servers.

The Software Developer's Mission

All the foregoing issues must be addressed by software developers who want to construct the "ideal software" for their customers. Considering this list as a whole, the following important design and architecture questions emerge:

  • How can applications be upgraded without destroying back-compatibility with prior versions?
  • How can system services be extended without an unnecessary dependency on a particular operating system?
  • How can tools from other vendors be utilized with the above points in mind?

The Search for Ideal Software: Some History

Various partial solutions to the problems of compatibility have emerged over the years. Data exchange among applications, for example, has been possible almost since the first microcomputer hit the streets. The exchange methods, however, were disparate and often unique to the particular applications sharing the data. The recent trend toward open solutions is no accident: Users need a standard mechanism for exchanging data.

Dynamic Data Exchange (DDE) goes at least part of the way to supplying a data exchange mechanism. Open Database Connectivity (ODBC) also addresses this need, allowing one database application to retrieve data from another.

Early Microsoft Technologies

The concept of library functions has been around for a long time. Common application and system services are packaged into files that can be accessed from various applications using standard function calls. This minimizes the size of application programs because not everything needs to be loaded at run-time. Using libraries can also reduce the complexity of programs, as standard services can be used and shared by different programs.

Microsoft's implementation of library functions-dynamic link libraries (DLLs)-does not address the "versioning problem," however. The problem is that it's easy for a newly installed program to update a library version, which can then interfere with some other program relying on the earlier version. This may occur with or without the user's knowledge. Even if the user has the option to overwrite an offending library, there is often no correct answer. Either the existing application breaks, or the new one doesn't work.

OLE 1 began to answer the need to share data. Users could either statically link data from one source into another, or they could dynamically embed data. This was an incomplete solution, though. To change the linked or embedded data, the source application must be accessed in another window.

Despite their insufficiencies, the DDE, DLLs, and OLE 1 technologies helped answer some of the user's needs. But in order for compatibility and extensibility to be achieved for users, the developer's design and architectural needs must be met. In some camps, Object Oriented Programming (OOP) was believed to be an answer; however, it can only partially address the developer's issues of back-compatibility and operating-system dependency. Let's take a look at why this is so.

Object Oriented Programming (OOP)

The development of Object Oriented Programming (OOP) represented a breakthrough in programming methods because it stressed classes (abstract sets of things such as mammals or automobiles), objects (instantiations of a class, such as a tiger being a concrete instance of a mammal, or a Toyota being a concrete instance of an automobile), and methods that can be applied to the objects.

When classes can be represented in an abstract manner, programmers can easily "subclass"-that is, extend the class to represent new nouns. Furthermore, only the interface (inputs needed by the objects and outputs produced by the various methods) is significant to the programmer. The low-level action mechanism of the predefined methods is unimportant. This concept of encapsulation or hiding is a critical property of OOP: The methods hide their modus operandi from the programmer, and only the interface is important. For example, the Java programming language presents a relatively simple set of classes and methods to produce working networked code, including GUI and threading capabilities.

For developers of "interchangeable" software, OOP has been considered a solution to at least some of the problems and points the way to other parts of the overall solution. OOP allows developers to create reusable software modules, so called because each module, or object, keeps its internal doings private from other software programs. The "outside world" sees only a set of function calls. Theoretically, this object can be taken out of one program and dropped into another, and the same function calls will work in each program.

Features of OOP

Let's take a look at a few of the key features of OOP, beginning with a simple example in C++. The start of our example is a simple class, CMyTime:

class CMyTime {
public:
     virtual LPSTR GetPublicTime();

private:
     LPSTR GetPrivateTime();
     char cbCookieTime[34];
     time_t ltime;
     struct tm *gmt;
};

We'll use this class to illustrate the first feature, encapsulation. In traditional programming, developers work with procedures and functions that are basically open to the rest of the program. With encapsulation, programming is done on an object level. An object consists of functions and data that are private to that object. The rest of the program only has access to the functions exposed by the object. Encapsulation lets the programmer reuse the object or change its internal workings, without having to worry about the effects on the rest of the program.

In this case, we are allowing public access to the function GetPublicTime(), which in our example will do the following:

LPSTR CMyTime::GetPublicTime() 
{
     LPSTR szCookieTime = GetPrivateTime();
     return (LPSTR)szCookieTime;     
}

This is an "open" function, in that the user can later override or change the way it behaves (we'll look at this capability shortly). The GetPublicTime() function calls a private function, however: GetPrivateTime(), which the user cannot change or override. Thus the developer of the GetPublicTime() function has used encapsulation to hide the internal doings of the GetPrivateTime() function from the user.

For example, consider the following code fragment:

     CMyTime * pMyTime;
     pMyTime = new CMyTime();
     LPSTR publicTime = pMyTime->GetPublicTime();
     LPSTR privateTime = pMyTime->GetPrivateTime(); 

Here the user has instantiated a new object derived from the CMyTime class. The attempt to use the GetPublicTime() function will work, but the attempt to use the private version will fail.

The above code fragment leads us to the second key feature of OOP, inheritance. Once an object is defined, new objects can be derived from it, inheriting the properties of the original object. These new objects then share the same original base functions and the same generic interface. In the preceding code fragment, all we did was create a new object of the type CMyTime. Now suppose we want to add to this class. We don't need to go back and modify the original CMyTime, but can simply derive our own class based on the original CMyTime:

class CYourTime : public CMyTime
{
public:
     void SomeNewFunction();
};

Now our new object has access not only to the original function, GetPublicTime() from the base class CMyTime, but also to our new function SomeNewFunction().

Although we're not going to examine virtual functions here, they lead into to the third OOP feature we want to explore: polymorphism. When disparate objects have the same generic interfaces, they are polymorphic. This means the developer can write code that talks to the generic interface, which will then determine the correct specific object to handle the task, providing a further layer of abstraction. For example, in our CMyTime class definition, we declared the following function:

LPSTR CMyTime::ShowTime(CMyTime *pTime) 
{
     return pTime->GetPublicTime();
}

This function accepts one parameter, a point to an object of the type CMyTime. Now, depending on the value of the parameter, we can call different implementations of the function. For example:

     CYourTime * pYourTime;
     pYourTime = new CYourTime();
     PublicTime = CMyTime::ShowTime(pYourTime);

will call the CYourTime version of the function. If we have declared a similar pointer to a CTheirTime object, we can simply replace the parameter to the ShowTime function to use this different implementation.

With these three features of OOP-encapsulation, inheritance, and polymorphism-you have a "plug-and-play" programming interface, in which defined objects can be plugged into a program and either used as is or customized. The Microsoft Foundation Classes (MFCs) and similar products from other vendors provide this technology. And though plug-and-play has fostered some great programs, it's not enough to solve the compatibility and data-exchange problems outlined at the start of this chapter.

Drawbacks of OOP

OOP's support for development of plug-and-play software modules applies only at the source code level; it is not a binary standard. This means there is no standard mechanism through which objects can work together. Modules from individual vendors may require their own interfaces to be developed at the source code level and may well exact a large penalty in development cost and time. Not only do different interfaces have to be considered, but the choice of programming language also becomes a potential stumbling block. A developer may wish to use an object written in an unfamiliar language, requiring a further investment in the learning curve.

The fact that compatibility is limited to the source code level also means that OOP does not address the important revision problem. When an object is updated, revised, and replaced, any programs using that object may need to be recompiled and possibly redistributed. While programmers are not unaccustomed to recompiling and relinking when they update an application, the chore of redistribution elevates their pain level. It raises the unpleasant possibility of DLLs' overwriting one another.

Certainly the software compatibility issue is partially solved by OOP, but it is not a complete answer. The general model provided by OOP is that of component software. This concept, broadly speaking, means that a developer can plug a component (from an arbitrary source) into an application. However, for the plug-in to work and the full functionality of the module to be accessed, the software module must be compatible and the interface clearly defined.

The COM Solution

COM, the solution developed by Microsoft, dispenses with the problems of recompilation and redistribution by making programmed objects, or components, compatible and reusable on a binary level. Compatibility at the binary level requires the support of the operating system. COM, however, differs from the traditional API style of sharing system services, in that COM describes the method by which objects connect. Subsequent to the connection, COM is no longer needed and stops consuming system resources. Compare this to an API, where an operating system has to manage the connection between the components at all times.

The Component Object Model also addresses another key factor necessary for object plug-and-play-namely, process management. An object that can be used by any client application must have some means to manage its own lifetime. When the client is done with an object, that object must know how to remove itself from memory, provided it is not being used by some other client application.

In spite of its complex role, the COM model is not a complicated technology. Actually, the fundamentals of the COM spec are quite simple and elegant:

  • COM is a binary standard, so the choice of programming language is (with one caveat) the programmer's choice. Coding, as well, is left up to the programmer, provided a program follows the COM rules. The caveat is that the programming language must be able to use indirect pointers to call functions in a COM object. Though this disallows some languages, a programmer is not forced into using C++ over C, for example.
  • Interfaces are uniquely identified across time and space via an identifier guaranteed to be unique: a 128-bit number. Once an interface is published with this identifier, the interface may not be revised. Updates are handled by adding new interfaces to the object, each with its own new identifier. This guarantees that client applications do not have to be recompiled. Because the clients only know about the interfaces in the previous version of an object, they simply don't know about the new features of the object.
  • An object is responsible for managing its own lifetime. This is accomplished via an internal reference count that essentially holds the number of clients connected at any time to the object. When the reference count drops to zero, the object can release itself from memory. It's a neat little mechanism that frees the developer from a lot of the headaches associated with providing sharable resources.
  • The connection mechanism between client and server is the same. It doesn't matter whether the server object is being used within the client process memory, outside the client's process boundaries, or across a network boundary. In fact, the client application for the most part doesn't need to know where the component is located, because COM will deal with establishing the connection. (The client application may wish to make provisions for using components across a network to deal with latency; however, this is not a requirement.)
  • The COM specification also provides a "flexible foundation for security at the object level..." Essentially, COM objects and data are subject to OS and application-level permissions. At the same time, since COM objects are in line with DCE standards, COM server applications have the option to implement their own security mechanisms.

Elements of the COM Connection

The connection between a component and a user of that component is commonly referred to as a contract. A contract implies that both parties agree up front to follow certain rules. This philosophy is what makes COM viable in the real world.

The general structure of the contract is simple. The component object must expose an interface called IUnknown, which declares three standard functions: QueryInterface, AddRef, and Release. The component object must then provide implementations of QueryInterface, AddRef, and Release, and the client must then use those functions to connect to the object and to inform the object when it is finished.

In the previous section, in our brief look at the features of OOP, we discussed the ability of objects to expose or hide functions, with the exposed functions often referred to as interfaces. We can create a new object derived from an existing object, with our new object inheriting the features of the original object. And polymorphism allows us to use the same function call but get different results based on the component we plug in.

COM is not OOP, but it does have the same look and feel. The COM object is derived from a base class called IUnknown. The I instead of the usual C is the conventional method for identifying this class as an interface or set of interfaces, and not an actual functional object. In the case of COM, IUnknown actually does nothing except to have declarations for the three member functions that define a COM object. It is left to the developer, when creating an object based on IUnknown, to then provide the implementations of the three functions QueryInterface, AddRef, and Release. As we'll see in our sample application, VC++ provides macros for creating standard implementations of these three functions.

The component/user connection is typically initiated with the QueryInterface function, which causes the server to look at its vtable, a virtual table of pointers to functions within an object. This table is created at run-time, by the object, when a client successfully locates and queries an object about its interfaces.

Assuming the server object supports the requested function, the client can use the function, but the client never sees the real address to the function. Instead, it receives from the object a pointer to a pointer in the vtable. This is one of the keys to COM: The component source can be recompiled, and the requested function can have a new address within that component. The client, however, because it never knows about the real address, doesn't need any changes to continue using the function. All the client sees is the pointer in the vtable, which is a fixed address by virtue of the unique identifier.

Here in simplified form are the steps of the generic process to establish a COM connection:

  1. The user program signals the COM API with the specific identity of the desired server object.
  2. COM first checks the processes currently in memory, and then the registry, attempting to find the appropriate object associated with the specified identity. If the object is found, COM passes back to the calling program a pointer to that object.
  3. The client asks the object to create a copy of itself, that is, to instantiate itself in memory. Rather than directly using the original object, the client always uses a copy, allowing for multiple connections to the object.
  4. The calling program attempts to get a pointer to a function within the object, and the component returns an answer. The answer is Null if the function does not exist in the component, or an address pointer to the function if it does exist.
  5. If a pointer is returned, the AddRef function is executed, incrementing the count on the object. At this point, with the client and component successfully connected and communicating, COM steps out of the way.
  6. When it is finished with the function, the client calls the Release function to decrement the count by one on the object. When the count reaches zero, the object is free to destroy itself, removing itself from memory.

Let's take a closer look at what the user has to do to implement a COM object, followed by an actual example. We'll start with the easy steps of acquiring a GUID and identifying our object to the operating system. Then we'll examine the more complex piece: implementing the required COM functions and talking to our COM object.

Getting a GUID

The first requirement to meet is a unique identity for the interface, provided through a globally unique identifier (GUID). A GUID is a 128-bit integer created via an algorithm specified as part of the OSF's (Open Software Foundation) Distributed Computing Environment. (In the OSF version, the identifier is a UID, but Microsoft decided that "unique across space and time" was too presumptuous and added the "global" qualifier.)

A GUID can represent one of two things: a class identity or CLSID, which uniquely identifies an object; or an IID, which represents an interface identity within a component. Note that the COM notion of a class is not the same as a C++ class. A COM class represents an object that supports one or more uniquely identified interfaces. Similarly, a COM interface is not to be taken for a C++ function. The COM interface is merely an entry point into an object, usually supporting one or more related functions.

There are various ways to acquire a GUID, including using a number of COM API functions. The developer will only need those functions in rare cases, though. It's easier to use either the much simpler uuidgen.exe console program provided with Developer Studio, or the guidgen.exe program, which provides a Windows front-end. The guidgen.exe utility (see Figure 3.1) can create GUIDs in several formats, with a function to copy the data to the clipboard for easy pasting into source code.

Figure 3.1 : The guidgen.exe utility program.

The GUID can be implemented in the source code in a number of ways, for example:

static const IID IID_IDate =
{ 0x7ad74677, 0xbe50, 0x11cf, { 0x9d, 0x92, 0xe4, 0x1f, 0xc1, 0x42, 0x0, 0x0 } };

After this the developer can forget about the number itself by using the static variable name. In order for COM to associate this GUID with the actual file that contains the object, the System Registry is used.

Getting into the System Registry

The System Registry contains detailed information about the local system, providing information formerly contained in Windows .INI files. For our purposes, we are concerned with how the registry is used to map a GUID to the physical location of a file. Figure 3.2 contains two snapshots of the registry on an NT system.

Figure 3.2 : Two views of the System Registry on an NT system.

The registry contains a number of "root" keys, including the key HKEY_CLASSES_ROOT, which contains the identifiers we are studying. Keys are represented by folder icons with plus signs, denoting that the key contains at least one or more subkey definitions.

In the upper window of Figure 3.2, you can see how a program name, or ProgID, is mapped to a Class ID. Note that, although a ProgID might be unique to a local machine and follow some program-naming convention, there is no way to know whether that name is used elsewhere. Hence the GUID, which is for all practical purposes unique.

The lower window shows the CLSID hierarchy, including the location of the file and its COM object type. The sample program we are developing is a DLL, so the file is registered as an InprocServer32-an in-process server, or one that is loaded into the calling program's process space. Other types of COM object include InprocHandler, a DLL with only a subset of its functions available; and LocalServer 32, an EXE server that runs in its own process space.

There are multiple paths available for getting a key into the Registry. For example:

  • The manual method is to run the program regedt32.exe and type in the appropriate keys and data. This is very tedious, and won't work anyway if the developer expects a program to be used on another machine.
  • Another option is to use the Win32 API functions to programmatically add Registry entries.
  • Create a text REG file that can be processed by the regedit.exe system program, which will add or edit the appropriate Registry entries. For example, the following file was used to register Cl10.DLL. This file was generated by the VC++ IDE.
REGEDIT
; This .REG file may be used by your SETUP program.
;   If a SETUP program is not available, the entries below will be
;   registered in your InitInstance automatically with a call to
;   CWinApp::RegisterShellFileTypes and COleObjectFactory::UpdateRegistryAll.


HKEY_CLASSES_ROOT\Cl10.Document = Cl10 Document
HKEY_CLASSES_ROOT\Cl10.Document\CLSID = {7AD74677-BE50-11CF-9D92-E41FC1420000}

HKEY_CLASSES_ROOT\CLSID\{7AD74677-BE50-11CF-9D92-E41FC1420000} = Cl10 Document
HKEY_CLASSES_ROOT\CLSID\{7AD74677-BE50-11CF-9D92-E41FC1420000}\InProcServer32 = 
D:\MSDEV\PROJECTS\CL10\DEBUG\CL10.DLL
HKEY_CLASSES_ROOT\CLSID\{7AD74677-BE50-11CF-9D92-E41FC1420000}\ProgId = Cl10.Document

TIP
A good source of information on the subject of REG files is buried in the Developer Studio's on-line books, under SDKs/Win32 SDK/OLE/OLE Programmer's Reference/Appendixes/Registering Object Applications-not exactly in plain sight.

Implementing the Three Required Functions

A COM object must implement three functions to fulfill its end of the contract: QueryInterface, AddRef, and Release, in that order.

QueryInterface, a member function of the class IUnknown, gives the client application a pointer in the vtable, to a pointer to a specific function within an object. If the object we are discussing is called, say, TestObj, first we would have a class definition for TestObj, derived from IUnknown:

class CTestObj : public IUnknown
     {
          public:
          int m_cRef; 

          // IUnknown interfaces
          HRESULT QueryInterface(REFIID iid, ** ppvObj);
          ULONG AddRef();
          ULONG Release();
          // Our function
          virtual void TestObj() = 0;
     };

Next, here is an implementation of QueryInterface:

HRESULT CTestObj::QueryInterface(REFIID iid, **ppvTestObj)
{
     if (iid == IID_IUnknown || 
IID_ITestObj)
     { 
          *ppvTestObj = this;
          AddRef();
          return NOERROR;
     }
     return E_NOINTERFACE);
}

If the interface we are looking for does not exist, QueryInterface sets the value of the pointer to Null and returns the "no interface" result; otherwise, QueryInterface returns a pointer to the function. This is not the only way to implement QueryInterface. The return value from the function, however, must be either a pointer or the Null value. This ensures that all clients' programs will not break-they either find the requested function or they don't.

QueryInterface will also call the next method, AddRef. AddRef increments the reference count on the interface, as follows:

ULONG CTestObj::AddRef()
{
     return ++m_Ref;
}

Thus, if the interface is not already being used and running in memory, the reference count will be 1; and if another request is subsequently made on the object, the count will be 2, and so on.

Of course, clients' programs cannot rely on this reference count to have any meaning, because they have no reliable way of knowing which programs, in separate process spaces, may be using the object. It is up to the object to manage itself in memory, which is the role of the third required function, Release.

Release decreases the reference count on an interface by one. When the reference count on an interface reaches zero, the object is free to destroy itself (remove itself from memory). Release must be called within the client program for each AddRef call, including the initial QueryInterface call.

ULONG CTestObj::Release()
{
     if (--m_ref == 0)
     {     delete this;
          return 0;
     }
     returm m_ref;
}     

The AddRef/Release mechanism is a very clever construct from the Microsoft team and is the key to memory/process management of a COM object. Through these two functions, a COM object knows when to remove itself from memory, and neither COM nor the OS has to manage the lifetime of objects on an ongoing basis. Once the connection is established between client and server, that's it-they are directly connected and there is no further overhead from either COM or the OS. (EXEs and DLLs free themselves from memory slightly differently, but the variations are inconsequential to the client application.)

Accessing the Object

Now that we've provided identities to our object and its function, the next step is to add a means for the object to make a copy of itself. Typically this is accomplished with the IClassFactory interface. IClassFactory has two methods, CreateInstance and LockServer, implemented as follows:

interface IClassFactory : IUnknown
     {
     HRESULT CreateInstance(IUnknown * pUnkOuter,
REFIID riid, void ** ppvObject);     

     HRESULT LockServer(BOOL fLock);
     }

Because the client has a pointer to the object via the successful QueryInterface call, the client can use that pointer to call the CreateInstance method. Assuming it succeeds, the client then has a pointer to the copy of the object in memory. It is through this pointer that the client can use functions within the object.

It is not a COM requirement that an object implement IClassFactory. The object can have its own method for creating a copy of itself. However, using IClassFactory::CreateInstance makes the developer's life simpler.

Implementing an Interface with MFC Macros

The Microsoft Foundation Class library includes a number of macros for streamlining the implementation of the required COM functions, via Interface Maps. In our sample in the next section, we use Interface Map macros to generate stock implementations of the QueryInterface, AddRef, and Release functions.

The Interface Map macros work similarly to Message and Dispatch Maps. Classes are derived from the class CCmdTarget, which includes definitions of the QueryInterface, AddRef, and Release functions. The developer will have to declare these functions but is saved the task of reimplementing the internal statements. For complete details, consult Technical Note #38 in Developer Studio.

A Sample COM Server Object

To give you a look at how these elements are put together, this section shows a simple AppWizard-generated DLL that implements a COM interface. In addition to the three functions required to implement IUnknown, the object includes a function that displays a message box on the screen with the date and time in it. This date and time function is the function that we want to provide a client application, and after the source code for the COM object we'll show some code from another application that accesses the function.

The first pair of files in Listings 3.1 and 3.2 contain the declaration and definitions of the three COM required functions, along with the function that our COM object will provide to a client application.


Listing 3.1 Date.cpp
// Date.cpp : implementation file
//

#include "stdafx.h"
#include "cl10.h"
#include "DDialog.h"
#include "Date.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CDate


IMPLEMENT_DYNCREATE(CDate, CCmdTarget)

CDate::CDate()
{
     timed=NULL;
     //     EnableAutomation();
     
     // To keep the application running as long as an OLE automation 
     //     object is active, the constructor calls AfxOleLockApp.
     
     AfxOleLockApp();
}

CDate::~CDate()
{
     // To terminate the application when all objects created
     //      with OLE automation, the destructor calls AfxOleUnlockApp.
     delete timed;
     AfxOleUnlockApp();
}


void CDate::OnFinalRelease()
{
     // When the last reference for an automation object is released
     // OnFinalRelease is called.  The base class will automatically
     // delete the object.  Add additional cleanup required for your
     // object before calling the base class.

     CCmdTarget::OnFinalRelease();
     //delete this;
}


BEGIN_MESSAGE_MAP(CDate, CCmdTarget)
     //{{AFX_MSG_MAP(CDate)
          // NOTE - the ClassWizard will add and remove mapping macros here.
     //}}AFX_MSG_MAP
END_MESSAGE_MAP()


// Note: we add support for IID_IDate to support typesafe binding
//  from VBA.  This IID must match the GUID that is attached to the 
//  dispinterface in the .ODL file.

// {7AD74677-BE50-11CF-9D92-E41FC1420000}
static const IID IID_IDate =
{ 0x7ad74677, 0xbe50, 0x11cf, { 0x9d, 0x92, 0xe4, 0x1f, 0xc1, 0x42, 0x0, 0x0 } };

BEGIN_INTERFACE_MAP(CDate, CCmdTarget)
     INTERFACE_PART(CDate, IID_IDate, Date)
END_INTERFACE_MAP()

// {7AD74678-BE50-11CF-9D92-E41FC1420000}
IMPLEMENT_OLECREATE(CDate, "Date", 0x7ad74678, 0xbe50, 
0x11cf, 0x9d, 0x92, 0xe4, 0x1f, 0xc1, 0x42, 0x0, 0x0)

/////////////

STDMETHODIMP_(ULONG) CDate::XDate::AddRef()
{
     TRACE("CDate::XDate::AddRef\n");
     METHOD_PROLOGUE(CDate, Date)
     return pThis->ExternalAddRef();
}

STDMETHODIMP_(ULONG) CDate::XDate::Release()
{
     TRACE("CDate::XDate::Release\n");
     METHOD_PROLOGUE(CDate, Date)
     return pThis->ExternalRelease();
}

STDMETHODIMP CDate::XDate::QueryInterface(REFIID iid,
                                             LPVOID* ppvObj)
{
//     TRACE(iid, "CDate::XDate::QueryInterface\n");
     METHOD_PROLOGUE(CDate, Date)
     return pThis->ExternalQueryInterface(&iid, ppvObj);
}

STDMETHODIMP_(void) CDate::XDate::ShowDate()
{
     TRACE("CDate::XDate::ShowDate\n");
     METHOD_PROLOGUE(CDate, Date)
     if((pThis->timed) == NULL){
          pThis->timed = new CDateDialog();
     }
     else{
          pThis->timed->ShowWindow(SW_SHOWNORMAL);
     }
     return;
}


Listing 3.2 Date.h
// Date.h : header file
//
// 
struct IDate : public IUnknown
{ 
     STDMETHOD_(void, ShowDate)() = 0;
};


/////////////////////////////////////////////////////////////////////////////
// CDate command target

class CDate : public CCmdTarget
{
     DECLARE_DYNCREATE(CDate)
private:
protected:
     CDate();           // protected constructor used by dynamic creation

// Attributes
public:

// Operations
public:


// Overrides
     // ClassWizard generated virtual function overrides
     //{{AFX_VIRTUAL(CDate)
     public:
     virtual void OnFinalRelease();
     //}}AFX_VIRTUAL

// Implementation
protected:
     CDateDialog *timed;
     virtual ~CDate();

     // Generated message map functions
     //{{AFX_MSG(CDate)
          // NOTE - the ClassWizard will add and remove member functions here.
     //}}AFX_MSG

     DECLARE_MESSAGE_MAP()
     DECLARE_OLECREATE(CDate)

     BEGIN_INTERFACE_PART(Date, IDate)
          STDMETHOD_(void, ShowDate)();
     END_INTERFACE_PART(Date)

     DECLARE_INTERFACE_MAP()
};

Now that we've defined and implemented the IUnknown functions and the interface to our date dialog, we can get to the task of implementing the date dialog itself. The files in Listings 3.3 and 3.4 are the code for the function that we want to provide a client application.


Listing 3.3 DDialog.cpp
// DDialog.cpp : implementation file
//

#include "stdafx.h"
#include "cl10.h"
#include "DDialog.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CDateDialog dialog


CDateDialog::CDateDialog(CWnd* pParent /*=NULL*/)
     : CDialog(CDateDialog::IDD, pParent)
{
     Create( CDateDialog::IDD, pParent );
     ShowWindow(SW_SHOWNORMAL);
     //{{AFX_DATA_INIT(CDateDialog)
     m_Date = _T("");
     m_Time = _T("");
     //}}AFX_DATA_INIT
     time=GetDlgItem(IDC_STATIC_TIMEVAL);
     date=GetDlgItem(IDC_STATIC_DATEVAL);
     CTime timeV=CTime::GetCurrentTime();
     time->SetWindowText(timeV.Format("%H : %M : %S" ));
     date->SetWindowText(timeV.Format( "%A, %B %d, %Y")); 
     SetTimer(1, 1000, NULL);
}

CDateDialog::~CDateDialog(){
}


void CDateDialog::DoDataExchange(CDataExchange* pDX)
{
     CDialog::DoDataExchange(pDX);
     //{{AFX_DATA_MAP(CDateDialog)
     DDX_Text(pDX, IDC_STATIC_DATEVAL, m_Date);
     DDX_Text(pDX, IDC_STATIC_TIMEVAL, m_Time);
     //}}AFX_DATA_MAP
}


BEGIN_MESSAGE_MAP(CDateDialog, CDialog)
     //{{AFX_MSG_MAP(CDateDialog)
     ON_WM_TIMER()
     ON_WM_DESTROY()
     //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CDateDialog message handlers

void CDateDialog::OnTimer(UINT nIDEvent) 
{
     // TODO: Add your message handler code here and/or call default
     CTime timeV=CTime::GetCurrentTime();
     time->SetWindowText(timeV.Format("%H : %M : %S" ));
     date->SetWindowText(timeV.Format( "%A, %B %d, %Y")); 
}

void CDateDialog::OnDestroy() 
{
     KillTimer(1);
     CDialog::OnDestroy();
}

void CDateDialog::OnOK() 
{
     // TODO: Add extra validation here
     
     //     CDialog::OnOK();
     //We do not want to destroy the dialog box (which is the default)
     //let's just hide it, so next time someone wants to see the date
     //it will be just shown
     ShowWindow(SW_HIDE);
}


Listing 3.4 DDialog.h
// DDialog.h : header file
//

/////////////////////////////////////////////////////////////////////////////
// CDateDialog dialog

class CDateDialog : public CDialog
{
// Construction
public:
     CDateDialog(CWnd* pParent = NULL);   // standard constructor
     ~CDateDialog();
// Dialog Data
     //{{AFX_DATA(CDateDialog)
     enum { IDD = IDD_DATE_DIALOG };
     CString     m_Date;
     CString     m_Time;
     //}}AFX_DATA


// Overrides
     // ClassWizard generated virtual function overrides
     //{{AFX_VIRTUAL(CDateDialog)
     protected:
     virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support
     //}}AFX_VIRTUAL

// Implementation
protected:
     CWnd *time,*date;
     // Generated message map functions
     //{{AFX_MSG(CDateDialog)
     afx_msg void OnTimer(UINT nIDEvent);
     afx_msg void OnDestroy();
     virtual void OnOK();
     //}}AFX_MSG
     DECLARE_MESSAGE_MAP()
};

We could have created this object as either a stand-alone executable or a DLL, both of which could be called by a client application. In this case we created a DLL version, and Listings 3.5 and 3.6 contain the functions that initialize this program as a DLL.


Listing 3.5 cl10.cpp
// cl10.cpp : Defines the initialization routines for the DLL.
//

#include "stdafx.h"
#include "cl10.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CCl10App

BEGIN_MESSAGE_MAP(CCl10App, CWinApp)
     //{{AFX_MSG_MAP(CCl10App)
          // NOTE - the ClassWizard will add and remove mapping macros here.
          //    DO NOT EDIT what you see in these blocks of generated code!
     //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CCl10App construction

CCl10App::CCl10App()
{
     // TODO: add construction code here,
     // Place all significant initialization in InitInstance
}

/////////////////////////////////////////////////////////////////////////////
// The one and only CCl10App object

CCl10App theApp;

/////////////////////////////////////////////////////////////////////////////
// CCl10App initialization

BOOL CCl10App::InitInstance()
{
     // Register all OLE server (factories) as running.  This enables the
     //  OLE libraries to create objects from other applications.
     COleObjectFactory::RegisterAll();

     return TRUE;
}

/////////////////////////////////////////////////////////////////////////////
// Special entry points required for inproc servers

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
     AFX_MANAGE_STATE(AfxGetStaticModuleState());
     return AfxDllGetClassObject(rclsid, riid, ppv);
}

STDAPI DllCanUnloadNow(void)
{
     AFX_MANAGE_STATE(AfxGetStaticModuleState());
     return AfxDllCanUnloadNow();
}

// by exporting DllRegisterServer, you can use regsvr.exe
STDAPI DllRegisterServer(void)
{
     AFX_MANAGE_STATE(AfxGetStaticModuleState());
     COleObjectFactory::UpdateRegistryAll();
     return S_OK;
}


Listing 3.6 cl10.h
// cl10.h : main header file for the CL10 DLL
//

#ifndef __AFXWIN_H__
     #error include 'stdafx.h' before including this file for PCH
#endif

#include "resource.h"          // main symbols

/////////////////////////////////////////////////////////////////////////////
// CCl10App
// See cl10.cpp for the implementation of this class
//

class CCl10App : public CWinApp
{
public:
     CCl10App();

// Overrides
     // ClassWizard generated virtual function overrides
     //{{AFX_VIRTUAL(CCl10App)
     public:
     virtual BOOL InitInstance();
     //}}AFX_VIRTUAL

     //{{AFX_MSG(CCl10App)
          // NOTE - the ClassWizard will add and remove member functions here.
          //    DO NOT EDIT what you see in these blocks of generated code !
     //}}AFX_MSG
     DECLARE_MESSAGE_MAP()
};

Finally, the IDE generates two files for us: an ODL file that generates the type library for our object, and a regedit file that registers our object. These files are in Listings 3.7 and 3.8.


Listing 3.7 cl10.odl
// cl10.odl : type library source for cl10.dll

// This file will be processed by the Make Type Library (mktyplib) tool to
// produce the type library (cl10.tlb).

[ uuid(7AD74677-BE50-11CF-9D92-E41FC1420000), version(1.0) ]
library Cl10
{
     importlib("stdole32.tlb");
};


Listing 3.8 cl10.reg
REGEDIT
; This .REG file may be used by your SETUP program.
;   If a SETUP program is not available, the entries below will be
;   registered in your InitInstance automatically with a call to
;   CWinApp::RegisterShellFileTypes and COleObjectFactory::UpdateRegistryAll.

; This is for the Class Factory

HKEY_CLASSES_ROOT\Cl10 = Cl10
HKEY_CLASSES_ROOT\Cl10\CLSID = {7AD74678-BE50-11CF-9D92-E41FC1420000}

HKEY_CLASSES_ROOT\CLSID\{7AD74678-BE50-11CF-9D92-E41FC1420000} = Cl10
HKEY_CLASSES_ROOT\CLSID\{7AD74678-BE50-11CF-9D92-E41FC1420000}\InProcServer32 = 
D:\MSDEV\PROJECTS\CL10\DEBUG\CL10.DLL
HKEY_CLASSES_ROOT\CLSID\{7AD74678-BE50-11CF-9D92-E41FC1420000}\ProgId = Cl10

; This is for the Date Interface

HKEY_CLASSES_ROOT\Cl10.Date = Cl10 Date
HKEY_CLASSES_ROOT\Cl10.Date\CLSID = {7AD74677-BE50-11CF-9D92-E41FC1420000}

HKEY_CLASSES_ROOT\CLSID\{7AD74677-BE50-11CF-9D92-E41FC1420000} = Cl10 Document
HKEY_CLASSES_ROOT\CLSID\{7AD74677-BE50-11CF-9D92-E41FC1420000}\InProcServer32 = 
D:\MSDEV\PROJECTS\CL10\DEBUG\CL10.DLL
HKEY_CLASSES_ROOT\CLSID\{7AD74677-BE50-11CF-9D92-E41FC1420000}\ProgId = Cl10.Document

This may seem like a lot of code just to implement one simple dialog function, but keep in mind that much of this gets handed to you by the Developer Studio AppWizard.

Now let's look at portions of a client application that utilizes our date object. This is another AppWizard-generated application. It's an SDI application, where we've added a toolbar item, Date, to access our object. When the user selects this menu choice, our COM object dutifully displays the date and time for us as shown in Figure 3.3.

Figure 3.3 : The COM object Date dialog.

Listings 3.9 and 3.10 are the source code for the View class of this client application; this was the only class we made relevant changes to in order to access our Date object from the application.


Listing 3.9 cl10eview.cpp
// cl10eView.cpp : implementation of the CCl10eView class
//

#include "stdafx.h"
#include "iunknown.h"
#include "cl10e.h"
#include "cl10eDoc.h"
#include "cl10eView.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif


// {7AD74677-BE50-11CF-9D92-E41FC1420000}
// trying 74678 which = CL10.Date
static const IID IID_IDate =
{ 0x7ad74677, 0xbe50, 0x11cf, { 0x9d, 0x92, 0xe4, 0x1f, 0xc1, 0x42, 0x0, 0x0 } };


/////////////////////////////////////////////////////////////////////////////
// CCl10eView

IMPLEMENT_DYNCREATE(CCl10eView, CView)

BEGIN_MESSAGE_MAP(CCl10eView, CView)
     //{{AFX_MSG_MAP(CCl10eView)
     ON_COMMAND(ID_DATE, OnDate)
     //}}AFX_MSG_MAP
     // Standard printing commands
     ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)
     ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint)
     ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CCl10eView construction/destruction

CCl10eView::CCl10eView()
{
     // TODO: add construction code here
     pClf=NULL;
     pUnk=NULL;
     pDate=NULL;
}

CCl10eView::~CCl10eView()
{
     if(pClf!=NULL)
          pClf->Release();
     if(pUnk!=NULL)
          pUnk->Release();
     if(pDate!=NULL)
          pDate->Release();

}

BOOL CCl10eView::PreCreateWindow(CREATESTRUCT& cs)
{
     // TODO: Modify the Window class or styles here by modifying
     //  the CREATESTRUCT cs

     return CView::PreCreateWindow(cs);
}

// CCl10eView drawing
void CCl10eView::OnDraw(CDC* pDC)
{
     CCl10eDoc* pDoc = GetDocument();
     ASSERT_VALID(pDoc);

     // TODO: add draw code for native data here
}

// CCl10eView printing
BOOL CCl10eView::OnPreparePrinting(CPrintInfo* pInfo)
{
     // default preparation
     return DoPreparePrinting(pInfo);
}
void CCl10eView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
     // TODO: add extra initialization before printing
}
void CCl10eView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
     // TODO: add cleanup after printing
}

// CCl10eView diagnostics
#ifdef _DEBUG
void CCl10eView::AssertValid() const
{
     CView::AssertValid();
}

void CCl10eView::Dump(CDumpContext& dc) const
{
     CView::Dump(dc);
}

CCl10eDoc* CCl10eView::GetDocument() // non-debug version is inline
{
     ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CCl10eDoc)));
     return (CCl10eDoc*)m_pDocument;
}
#endif //_DEBUG

// CCl10eView message handlers
void CCl10eView::OnDate() {
     CLSID clsid = IID_IDate;
     HRESULT hr;
     //we do not to create the interface over and over agian
     //lets just reuse it
     if(pDate!=NULL){
          pDate->ShowDate();
          return;
     }
     if ((hr = ::CLSIDFromProgID(L"CL10", &clsid)) !=NOERROR){ 
          TRACE("Could not get the class ID");
          return;
     }


    TRACE("hr from CLSIDFromProgID=%s\n\n", hr);

     if ((hr = ::CoGetClassObject(clsid, CLSCTX_INPROC_SERVER,
          NULL, IID_IClassFactory,(void **) &pClf)) !=NOERROR){ 
          TRACE("Error getting IClassFactory\n", &clsid);
          TRACE("And hr = %d\n\n", hr);
          return;
     }

     if((pClf->CreateInstance(NULL, IID_IUnknown, (void**) &pUnk))==S_OK){
          if((pUnk->QueryInterface(IID_IDate,(void**)&pDate))==S_OK){
               pDate->ShowDate();
          }else{
               TRACE("Could not get the IDate Interface from Date DLL\n");
               pClf->Release();
               pUnk->Release();
          }
     }else{
          TRACE("Could not get the IUnknown Interface from Date DLL\n");
          pClf->Release();
     }
}


Listing 3.10 cl10eview.h
// cl10eView.h : interface of the CCl10eView class

class CCl10eView : public CView
{
protected: // create from serialization only
     LPCLASSFACTORY pClf;
     LPUNKNOWN pUnk;
     struct IDate : public IUnknown{
          STDMETHOD_(void, ShowDate)()=0;
     } *pDate;

//     IDate *pDate;
     CCl10eView();
     DECLARE_DYNCREATE(CCl10eView)

// Attributes
public:
     CCl10eDoc* GetDocument();

// Operations
public:

// Overrides
     // ClassWizard generated virtual function overrides
     //{{AFX_VIRTUAL(CCl10eView)
     public:
     virtual void OnDraw(CDC* pDC);  // overridden to draw this view
     virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
     protected:
     virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);
     virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);
     virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);
     //}}AFX_VIRTUAL

// Implementation
public:
     virtual ~CCl10eView();
#ifdef _DEBUG
     virtual void AssertValid() const;
     virtual void Dump(CDumpContext& dc) const;
#endif

protected:
// Generated message map functions
protected:
     //{{AFX_MSG(CCl10eView)
     afx_msg void OnDate();
     //}}AFX_MSG
     DECLARE_MESSAGE_MAP()
};

#ifndef _DEBUG  // debug version in cl10eView.cpp
inline CCl10eDoc* CCl10eView::GetDocument()
   { return (CCl10eDoc*)m_pDocument; }
#endif

Type Libraries

In the listings for our sample COM object, the IDE created a file with the extension .ODL, which stands for Object Definition Language. This complete minilanguage is used to define information that can go in a program's header, library, and help files. The ODL file can then be processed by the program mktyplib.exe, which converts the text file into a binary TLB file. The resulting binary contains a series of structures that can be used by other programs to learn about the objects, interfaces, and other information in a program.

Type libraries are required for some types of OLE technologies, including OLE Automation. Required or not, however, it is generally a wise idea to create a type library for a program. Otherwise, without access to the source code or at least the header files, there is no way for a user to discover what objects or interfaces are supported by an application.

Developer Studio includes a tool called OLE Object View, and the first beta release of the ActiveX SDK contains an improved stand-alone version of this tool. Figure 3.4 shows an OLEViewer screen for an OLE control.

Figure 3.4 : An OLE control's interfaces, ID numbers, and other info, as displayed by the OLEViewer utility.

This is a wealth of information for the programmer to have available. For this particular control, we also have a copy of the binary type library file, allowing us to discover methods within the interfaces in the control. Figure 3.5 shows a few of the methods in the Iftpct interface.

Figure 3.5 : A partial listing of the methods in the Iftpct interface.

Fundamental OLE Services

Now that we've taken a look at the Component Object Model, which underlies OLE technology, let's move up a level and look at the fundamental OLE services that are built on top of COM. These "low-level" services are Persistent Naming, Persistent Storage, and Uniform Data Transfer.

We are referring to these as low-level because they build directly on COM, and other OLE technologies build on these three fundamental services. Depending on your needs, the services might not be the first things that come to mind when you're discussing ActiveX, and you may never need to do any development at all with these services. They are worth understanding, however, because they are the foundation on which the higher-level OLE services are built.

Intelligent Names

The word moniker is a synonym for name. In COM, a moniker can represent five different types of objects, the simplest being a filename. A moniker can also refer to a chunk of data within a file or document, such as a range of cells in a spreadsheet. Monikers can also refer to objects that perform an action-an SQL query, for example. What monikers do, then, is add intelligence to a resource name. This helps an application, upon encountering a moniker, to know not only the location of the object of the moniker, but also how to use that object.

The IMoniker interface is derived from the IPersistStream interface (which in turn is derived from the IPersist interface). As a specific implementation of the IPersistStream interface, IMoniker is used to locate and activate COM objects.

Prior to the development of ActiveX, there were five monikers:

  • File monikers refer to files.
  • Item monikers refer to objects within a file (more specifically, containers).
  • Pointer monikers are used to reference objects in memory (nonpersistent objects).
  • Anti-monikers are used to create relative versions of the other types of monikers; they work much like relative path names.
  • The important composite monikers can contain combinations of any type of moniker, including other composite monikers.

To these monikers, ActiveX adds two more: the asynchronous moniker and an implementation of that, the URL moniker. The asynchronous moniker is not an actual moniker but rather a specification for monikers that need to retrieve data asynchronously, such as across a network. With synchronous file transfers, the calling application blocks while the transfer occurs. Typically this blocking is short lived and of no consequence. With network transfers, though, latency becomes an important issue. The async moniker lets the transfer occur in the background and does not block the calling application. Thus other processes and user activities can occur.

Persistent Storage

Persistent Storage is also known as Structured Storage, which is a more descriptive name. OLE 1 was developed to solve the problem of creating and storing compound documents, or documents that could contain data (embedding) or references (linking) to data from multiple sources, including various types of source files.

In the traditional file system, a disk is organized into directories and files (we'll ignore such things as partitions for the purpose of this discussion). Directories can contain not only files but subdirectories and, with some file systems, links to other files or directories. In any case, the system can only look at directory and file objects, but no further.

Structured Storage goes beyond this by providing a "file system within a file," via the IStream and IStorage interfaces. The IStorage interface contributes the functions necessary to build the "file structure" within what appears to the user as a unified, individual file. The IStream interface contributes the functions needed to store the actual data pointed to by the IStorage functions.

In keeping with the network-oriented approach of ActiveX, a specification for a "new" type of compound file was developed: the Asynchronous Compound File (ACF). This is not really a new file type, however. You'll remember that one of COM's philosophies is to be extensible only in ways that do not break existing client applications. Thus, the existing compound file architecture was extended by adding new interfaces that are beneficial to knowledgeable client programs .

Two new interfaces, then, enable us to have an Asynchronous Compound File. One is the IFillLockBytes interface, which can be used by programs that use the URL moniker to download data asynchronously. In tandem with this interface, IProgressNotify can be used to notify the client application of the status of the asynchronous transfer.

Uniform Data Transfer

Rounding out the set of core COM services is Uniform Data Transfer (UDT), which provides a layer of abstraction for exchanging data stored in specified (usually standard) formats. UDT is implemented through the IDataObject interface, which also provides services for notifying host applications of changes to the data via the IAdviseSink interface. In addition, the interface allows for specifying the medium through which data is transferred (for example, volatile or nonvolatile storage).

You are likely familiar with the concept of uniform data transfer in the drag-and-drop capabilities of Windows. Although the ActiveX SDK has no specific new interfaces for UDT, it will be possible, once the beta dust settles, to put together the capability to drag data from an Internet source down to a local desktop target object.

URL Open Stream Functions

To make everyone's job a bit easier, Microsoft has encapsulated much of the functionality of retrieving data via URL monikers into the easy-to-use URL Open Stream (UOS) functions.

The essential characteristic of UOS functions is that the calling application need only implement one interface, IBindStatusCallback, which we have already seen at work in the preceding translation of the Progress application. A program that utilizes UOS does not need to implement many of the methods that were required with URL moniker. In addition, it is possible to use UOS to do HTTP Post requests.

The following functions make up the URL Open Stream service:

UOS FunctionAction
URLOpenStreamDownloads data from network as it becomes available and delivers it immediately to calling program. Retrieves data from cache if it's there, and adds new data retrieved to the cache.
URLOpenBlockingStreamDownloads data from network as requested by the client and delivers data in a blocking fashion. Retrieves data from cache if it's there, and adds new data retrieved to the cache.
URLDownloadToFileDownloads data from the network and saves it to a specified file. Does not look at or save data to the cache.
URLOpenPullStreamSame as the blocking function, except it doesn't block. If not enough data is available to satisfy the client request, this function can return a "pending" notification and remain open.
URLOpenHttpStreamA generic function that can be used like one of the other four UOS functions, while also allow for complex stream requests such as making an HTTP Post or Put request.

ActiveX Hyperlink Support

Hyperlinks are one of the key features contributing to the popularity of Web browsers, and ActiveX makes it possible to add this feature to other types of applications. Building on the URL Moniker technology, ActiveX adds hyperlink support in two flavors: simple and OLE. Because this support is built with URL monikers, the user can navigate not only to types of documents other than HTML, but also to locations within OLE Document Objects. For example, it's possible to navigate to a location within an OLE document, such as a range of cells in a spreadsheet, with the location provided by a composite URL moniker. To round out the browser model, history and bookmark lists can also be maintained.

Following are the new functions that provide the simple version of ActiveX hyperlink support:

Hyperlink FunctionAction
HlinkSimpleNavigate-ToStringCauses the application to jump to the document or object indicated by the string.
HlinkSimpleNavigate-ToMonikerThe specification is vague about this one, merely repeating the definition for the ToString version. "Jumping to a moniker" implies that this function lets you jump to a reference within a document, such as a name group of spreadsheet cells.
HlinkSimpleNavigate-StringSame as the ToString version, except most parameters are set to Null.
HlinkSimpleNavigate-MonikerSame as the ToMoniker version, except most parameters are set to Null.
HlinkGoBackJumps to the previous link in the navigation stack, if there is a link to go back to.
HlinkGoForwardJumps to the next link in the navigation stack, if there is a link to go forward to.

The GoBack and GoForward functions operate within the context of the current application frame. If a navigate function points to a document or object not viewable in the current frame, then a separate application window opens and the new link is not added to the navigation stack. For example, if the application is an Explorer-like application that displays HTML, and the HLinkNavigateToString function is used to navigate to a Word document, then the Word application will start up in a separate window, which will become the user's active window.

This simple version of hyperlink support is a subset of the OLE version of hyperlink support, which potentially increases the developer's control over the navigation stack, window context, and so forth. As discussed earlier, these extended objects and interfaces are built on top of the Uniform Data Transfer and Monikers services. However, as of this writing, the OLE version of hyperlinks is still under review and subject to revision. Microsoft suggests that developers stick with the simple version.

The ActiveX SDK contains an ActiveX control which implements hyperlinking, and in Chapter 5we will show an example of how to use this control in a Web page.

The COM Cornerstone

The Component Object Model and the core services that build on it provide the foundation for all of the higher-level technologies that both developers and users are more familiar with. Many of the ActiveX technologies discussed in recent press are either existing services built on top of COM or extensions to those services.

OLE User Services

The architectures that build on the core COM services can be considered "OLE user services"-that is, services an end user will typically encounter and directly utilize:

  • OLE Automation is a means for one application to programmatically control another application that is enabled to be a COM server application. You are likely familiar with this technology in the form of Visual Basic for Applications. Another implementation, VBScript, is explored in Chapter 2
  • Drag & Drop brings the power of a GUI to the forefront of the user's awareness by streamlining cut/copy/paste operations within or between windows and applications.
  • Both Linking and Embedding allow the user to either statically (embedding) or dynamically (linking) share data between applications.
  • In-Place Activation, also known as Visual Editing, lets the user access the functionality of a server program from within another program.

These user-oriented OLE services have been combined in the last couple of years to provide two powerful technologies: OLE Controls and Document Objects. OLE Controls will be discussed in Chapter 5.

Document Objects and Containers

You are by now no doubt familiar with the Windows 95 Office Binder application that forms the centerpiece of Microsoft Office. The Binder is a document container. That means all the application does is host other document objects, allowing the user to place multiple files of various types into the container and edit the files within the container. The Binder saves the entire collection of objects into one file, using Persistent Storage. The user only has to deal with one window frame when editing any of the objects embedded or linked into the binder frame.

Office Binder is the prime example of Microsoft's "document-centric" philosophy, allowing the user to just focus on the task at hand rather than worrying about managing multiple windows and applications. Everything the user needs or wants to do can be made available within a single application frame. The Binder is the showcase application for Microsoft's COM and OLE technologies. It incorporates all of the building blocks discussed in this chapter.

For Further Reading

This chapter has given you an admittedly brief overview of the Component Object Model, one that hardly does it justice. If you knew nothing before of COM and the low-level OLE technologies, you now have at least a feel for those technologies and hopefully are not intimidated by them.

The definitive, authoritative work on COM and OLE is Kraig Brockschmidt's Inside OLE, published by Microsoft Press. This mighty volume is over 1,100 pages and makes for fascinating reading; not one page is wasted. The reader is well advised to keep this book handy as a cure for boredom.



HomeAbout UsSearchSubscribeAdvertising InfoContact UsFAQs
Use of this site is subject to certain Terms & Conditions.
Copyright (c) 1996-1998 EarthWeb, Inc.. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of EarthWeb is prohibited.
Please read the Acceptable Usage Statement.
Contact reference@developer.com with questions or comments.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.

Click here for more info

Click here for more info