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 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.
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.
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?
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.
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.
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.
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.
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:
- The user program signals the COM API with the specific identity
of the desired server object.
- 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.
- 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.
- 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.
- 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.
- 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.
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.
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.
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 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.
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.
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 Function | Action
|
URLOpenStream | Downloads 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.
|
URLOpenBlockingStream | Downloads 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.
|
URLDownloadToFile | Downloads data from the network and saves it to a specified file. Does not look at or save data to the cache.
|
URLOpenPullStream | Same 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.
|
URLOpenHttpStream | A 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.
|
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 Function | Action
|
HlinkSimpleNavigate-ToString | Causes the application to jump to the document or object indicated by the string.
|
HlinkSimpleNavigate-ToMoniker | The 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-String | Same as the ToString version, except most parameters are set to Null.
|
HlinkSimpleNavigate-Moniker | Same as the ToMoniker version, except most parameters are set to Null.
|
HlinkGoBack | Jumps to the previous link in the navigation stack, if there is a link to go back to.
|
HlinkGoForward | Jumps 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 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.
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.
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.
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.