The ActiveX Template Library (ATL) was created to answer the need for lightweight and fast ActiveX COM components. In addition to creating COM Objects and Controls, ATL can be used to create Automation Servers with a minimum of effort and overhead. ATL consists of a set of template classes that are a relatively new concept to most developers; however, the focus of this chapter is on creating Automation Servers. Please see your favorite C++ book or manual for more information regarding the definition and use of templates.
ATL is intended to solve the problems of COM com-ponent development and is not an attempt to be an all-encompassing class library for creating applications. ATL was designed to work with other class libraries, such as MFC or the Standard Template Library (STL), that provide basic classes for string manipulation, array, lists, memory management, and so on. You, the developer, have the freedom to choose which class library best suits your needs and combine it with ATL to create small, fast COM applications. In this chapter, you will create a simple in-process Automation Server using ATL and MFC for logging string data to a file. The use of MFC will allow you to concentrate on how to implement your server rather than on how to find alternative methods to functions and classes you are familiar with. You could just as easily opt not to use MFC, but then you would have to use the Windows API directly to implement anything related to the OS. As it is, using MFC will not add much to your application in terms of size.
As you proceed through the chapter, you will expand on your implementation, highlighting some of the more advanced concepts of Automation Server creation using ATL.
When creating an Automation Server, the first step is to create a basic project upon which you will build your application's features and functionality. Like MFC, ATL has an AppWizard for creating the basic ATL project.
From the File menu, select the New menu item. In the New dialog
(see fig. 4.1), select the Projects tab. The Projects tab allows you the
opportunity to define several aspects of how the application will be created,
for example, the type of application to create, the name of the application, and
the location where you want the project created. For the type, select ATL COM
AppWizard; enter the Project name ATLServer, and the Location will
be C:\que\ActiveX\ ATLServer. Click the OK button to start the ATL COM
AppWizard so you can further define the properties of your server. FIG.
4.1
Define the new ATL server project in the New dialog.
In the ATL COM AppWizard -- Step 1 of 1 dialog (see fig. 4.2), select a
Server Type of Dynamic Link Library (DLL), and check the Support
MFC check box. Click the Finish button to continue. FIG.
4.2
Define the basic architecture of the ATL COM object with the
ATL COM AppWizard.
The New Project Information dialog (see fig. 4.3) is used to confirm the
settings that were selected for the project prior to the creation of the actual
source files. This is the last step in the ATL COM AppWizard. "But wait," you
say, "I haven't defined any of my server properties." The ATL COM AppWizard
takes a slightly different approach from that of MFC. Only the basic source
files are created with the AppWizard, the remainder of the project is defined by
the ATL ObjectWizard--thus allowing for much better control of the project
implementation versus MFC since the developer can add any number of ActiveX
Servers, Controls, or plain COM objects after the basic project is created.
After you have confirmed your project settings, click the OK button to close the
ATL COM AppWizard and create the ATLServer project. FIG.
4.3
Confirm the new project settings with the New Project
Information dialog.
The ATL AppWizard generates all of the basic files that are needed to create a DLL-based ATL Automation Server. Table 4.1 lists all of the files that are created and a brief explanation of their purpose.
Filename | Description |
ATLServer.clw | VC++ project file. |
ATLServer.cpp | The main application source file and entry point for the DLL. |
ATLServer.def | Standard application DEF file. This file contains the function export declarations needed for all in-process servers. |
ATLServer.dsp | VC++ project file. |
ATLServer.dsw | VC++ project file. |
ATLServer.idl | Interface Definition Language file, which is used to create the type library for the server. |
ATLServer.ncb | VC++ project file. |
ATLServer.rc | Standard resource file. |
StdAfx.cpp | Standard precompiled header source file. |
ATLServerps.def | Proxy/Stub DLL definition file. |
ATLServerps.mk | Proxy/Stub MK file. |
StdAfx.h | Standard precompiled header file. All of the MFC-specific include files are added here. |
To be an Automation Server, an application must contain at
least one or more IDispatch-based interfaces. You will use the ATL
ObjectWizard to add your automation server interfaces to your application. From
the Insert menu, select the New ATL Object menu item. Within the
ATL ObjectWizard dialog (see fig. 4.4), select the Objects item in the left
panel to display the types of ATL components that can be added. Your
implementation will be an Automation Server, so select the Simple Object icon.
See the ATL documentation for more information on the other types of objects
that can be created. Click the Next button to continue.
FIG.
4.4
Select the type of ATL object to add to your project.
The next dialog is the ATL Object Wizard Properties dialog, which is used to
define the specific properties of the new object that will be added to your
project. Select the Names tab (see fig. 4.5), and in the Short Name edit
field, type Tracker; the remainder of the edit fields will automatically update,
reflecting the short name that you added. The other fields can be changed, but
in this case, you will use the default values.
FIG.
4.5
Define the name of the new control object.
Select the Attributes tab so that you can define the attributes of the server
object (see fig. 4.6). Check the Support ISupportErrorInfo check box to
add OLE rich error. You can add events to the ATL object by checking the Support
Connection Points check box; however, for the purposes of the sample, you will
not. Leave the remainder of the settings on the Attributes tab at their default
values. Click OK to continue and to add the object to the project.
FIG.
4.6
Define the attributes of the new server object.
Chapters 8 and 9 go into detail about adding events to an ATL control; the same process is used for an ATL server. At the time this book was being written, one drawback to having events was that only VB5 was built to take advantage of them.
The ATL ObjectWizard added the files, Tracker.h, Tracker.cpp, and Tracker.rgs to the project. Tracker.h and Tracker.cpp are the implementation files for your server object. Tracker.rgs is the registry script file that is used to register your server in the registration database. You will learn more about the Tracker.rgs file in the section regarding server registration a little later in this chapter.
Before continuing with your server implementation, your newly created IDL
file deserves a quick review. Listing 4.1 shows the basic IDL file that is
generated by the AppWizard.
TIP: For clarity, you should add the hidden attribute to the IDispatch-based interfaces defined in the server (see Listing 4.1). This step prevents tools like VB from displaying both the IDispatch interface and its related CoClass within the VB Object browser. The CoClass is the only interface that needs to be visible since it is the only interface that VB uses. Using the IDispatch interface in VB will result in errors.
The most obvious difference between the ODL from the MFC sample and the
IDL of this sample is the location of the IDispatch interface relative
to the rest of the type library description. When using IDL, you must
declare the interfaces that will generate the C++ source files outside of the
library declaration. For the ODL, this step is not necessary. Other than a few
minor language differences, the IDL and ODL are identical in terms of syntax and
organization.
import "oaidl.idl";
// ATLServer.idl : IDL source
for ATLServer.dll
// // This file will be processed by the MIDL tool to
// produce the type library (ATLServer.tlb) and marshalling code. [
object,
uuid(03699612-809E-11D0-BEFF-00400538977D),
dual,
helpstring("ITracker Interface"),
pointer_default(unique),
hidden
]
interface ITracker : IDispatch
{
};
[
uuid(03699601-809E-11D0-BEFF-00400538977D),
version(1.0),
helpstring("ATLServer 1.0 Type Library")
]
library ATLSERVERLib
{
importlib("stdole32.tlb"); [
uuid(03699613-809E-11D0-BEFF-00400538977D),
helpstring("Tracker Class")
]
coclass Tracker
{
[default] interface ITracker;
}; };
Before another application can use the server, however, OLE has to know where to find the server, which is done through the system registry. All ActiveX components that are publicly available to other applications must support registration and must create valid registry entries.
ActiveX components have one or more registry entries that are used to describe various aspects of the application and how it can be used. The registry is critical to the successful launching and using of ActiveX components.
All inproc ActiveX components expose registration support via two exported functions: DllRegisterServer and DllUnregisterServer. For information regarding the registration of local servers, see Chapter 3.
The basic registration and unregistration support for the server is already implemented by ATL. You are not required to make code changes or additions to support it. Remember that the MFC implementation allows only for registering the server and does not support unregistration.
Unlike MFC which uses a set of constants, ATL relies on resource information in the form of a registry script file to define the information that is added to the registry database. The registry script file is added automatically to the project when the server object is added; there is one script file for each server object.
The registry script file(s) are compiled into the server project as resources and can be viewed in binary form in the resource editor. The files, which have the extension .rgs, are normal text files that can be edited within the IDE. For more information about the use of registry script files and their particular syntax, see the VC++ books online subject "Registry Scripting Examples--ActiveX Template Library, Articles." Listing 4.2 shows the registry script file for the CTracker server object that you added.
HKCR
{
Tracker.Tracker.1 = s `Tracker Class'
{
CLSID = s `{03699613-809E-11D0-BEFF-00400538977D}'
}
Tracker.Tracker = s `Tracker Class'
{
CurVer = s `Tracker.Tracker.1'
}
NoRemove CLSID
{
ForceRemove
{03699613-809E-11D0-BEFF-00400538977D} = s `Tracker Class'
{
ProgID = s
`Tracker.Tracker.1'
VersionIndependentProgID = s `Tracker.Tracker'
ForceRemove `Programmable'
LocalServer32 = s `%MODULE%'
}
}
}
NOTE: Unfortunately, at the time of the writing of this book, the ATL ClassWizard created the Tracker.rgs file incorrectly. Hopefully, this problem will be fixed by the time the product is released. Listing 4.3 shows the updated version of the Tracker.rgs file with the mistakes corrected. The first correction was to change the ProgID to ATLServer.Tracker, reflecting the parent module and the subordinate object. The next correction was to add the CLSID entry to the version- independent ProgID section. This may have been intentional on the part of the ATL team. The last and definitely most significant problem was the fact that the CLSID section listed the server as LocalServer32 and not InprocServer32. After making the corrections, the server registered and ran as expected.
HKCR
{
ATLServer.Tracker.1 = s `Tracker
Class'
{
CLSID = s `{03699613-809E-11D0-BEFF-00400538977D}'
}
ATLServer.Tracker = s `Tracker Class'
{
CLSID = s
`{03699613-809E-11D0-BEFF-00400538977D}'
CurVer = s `ATLServer.Tracker.1'
}
NoRemove CLSID
{
ForceRemove
{03699613-809E-11D0-BEFF-00400538977D} = s `Tracker
{
ProgID = s
`ATLServer.Tracker.1'
VersionIndependentProgID = s `ATLServer.Tracker'
ForceRemove `Programmable'
InprocServer32 = s `%MODULE%'
}
} }
Since the server is used to output data to a file, you first need to add some support code to the application before adding its methods and properties.
Listing 4.4 shows the changes and additions that need to be made to the class header file. First add a destructor to the class and remove the constructor implementation--you will add it to the source file later. Then add a set of member variables for storing the file handle and timer information that will be used throughout the server implementation.
/////////////////////////////////////////////////////////////////////////////
// CTracker
class ATL_NO_VTABLE CTracker :
public
CComObjectRootEx<CComObjectThreadModel>,
public
CComCoClass<CTracker, &CLSID_Tracker>,
public ISupportErrorInfo,
public IDispatchImpl<ITracker, &IID_ITracker,
&LIBID_ATLSERVERLib>
{
public:
// constructor
CTracker();
// destructor
~CTracker(); DECLARE_REGISTRY_RESOURCEID(IDR_TRACKER)
BEGIN_COM_MAP(CTracker)
COM_INTERFACE_ENTRY(ITracker)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP() // ISupportsErrorInfo
STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid); // ITracker
public:
protected:
FILE * m_fileLog;
long m_lTimeBegin;
long m_lHiResTime;
long m_lLastHiResTime; };
The next step is to update the source file for the class. Add the include file, mmsystem.h, before the Tracker.h include file (see Listing 4.5). This file is for the timer functions that you take advantage of throughout the server implementation.
The following line is added within the constructor:
AFX_MANAGE_STATE(AfxGetStaticModuleState());
If you are using MFC with ATL, you must add this line as the first line in every function that uses MFC. This line is needed to stabilize the state information that is used by MFC. If you do not perform this step, the server will not function correctly and can result in errors during program execution.
You must call the method AfxOleLockApp() to ensure that the application will not be unloaded from memory until the reference count reaches zero.
Next you create a high resolution timer and store its current value in your member variables. The timer is useful for determining the number of milliseconds that have passed since the last method call was made. The timer output is great for tracking the performance of a particular action or set of actions.
You then get the current date and create a filename with the format YYYYMMDD.tracklog. After successfully opening the file, you output some start-up data to the file and exit the constructor.
The destructor does the exact opposite of the constructor. If there is a valid file handle, you write some closing information to the file and close it. Next you terminate the timer. Remember to call the function AfxOleUnlockApp() to allow the application to be removed from memory.
// Tracker.cpp : Implementation of CTracker
#include "stdafx.h"
#include "ATLServer.h"
// needed for the high
resolution timer services
#include <mmsystem.h>
#include
"Tracker.h"
/////////////////////////////////////////////////////////////////////////////
// CTracker STDMETHODIMP CTracker::InterfaceSupportsErrorInfo(REFIID riid)
{
static const IID* arr[] =
{
&IID_ITracker,
};
for
(int i=0;i<sizeof(arr)/sizeof(arr[0]);i++)
{
if
(InlineIsEqualGUID(*arr[i],riid))
return S_OK;
}
return S_FALSE;
} // constructor
CTracker::CTracker()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()); // To keep the application
running as LONG as an
// OLE automation object is active
::AfxOleLockApp(); // setup our timer resolution
m_lTimeBegin =
timeBeginPeriod(1);
m_lHiResTime = m_lLastHiResTime = timeGetTime(); // get
the current date and time
CTime oTimeStamp = CTime::GetCurrentTime();
CString cstrFileName; // create a file name based on the date
cstrFileName.Format(_T("%s.tracklog"), (LPCTSTR) // open a file
m_fileLog = fopen(cstrFileName, _T("a"));
// if we have a file
handle
if(m_fileLog)
{
// output some starting information
fprintf(m_fileLog, _T("************************\n"));
fprintf(m_fileLog,
_T("Start %s\n"),
(LPCTSTR) oTimeStamp.Format("%B %#d, %Y, %I:%M %p"));
fprintf(m_fileLog, _T("\n"));
}
} // destructor
CTracker::~CTracker()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
// if we have a file handle
if(m_fileLog)
{
// output some closing
information
CTime oTimeStamp = CTime::GetCurrentTime();
fprintf(m_fileLog, _T("\n"));
fprintf(m_fileLog, _T("End %s\n"),
oTimeStamp.Format("%B %#d, %Y, %I:%M %p"));
fprintf(m_fileLog,_T("************************\n")); // close the file
fclose(m_fileLog);
}
// if we have valid timer services
if(m_lTimeBegin == TIMERR_NOERROR)
// reset the timer to its original
state
timeEndPeriod(1); // no longer necessary to keep the application in
memory
::AfxOleUnlockApp();
}
Finally you update the build settings for the project. Since the sample implementation is using some timer functions defined in mmsystem.h, you also need to be linked with the appropriate library file that contains their implementation. Under the Project menu, select the Settings menu item. In the Project Settings dialog, from the Settings For drop-down list box, select the All Configurations entry. Select the Link tab, and add the file winmm.lib to the Object/library modules edit field. Click OK to close the dialog.
The basic support code needed for the sample implementation is now added. The server will open a file in its constructor and leave the file open during its entire lifetime. When the server is destroyed, the destructor will be called, and the file will be closed.
The following section describes how to make the sample more meaningful by adding methods and properties that are used to output data to the open file.
An automation method consists of zero to n parameters and may or may not have a return value. The term method is synonymous with function or subroutine, depending on the particular language you are familiar with. Since your server is IDispatch-based, you are limited to a specific set of data types. Only those data types that are valid VARIANT data types can be passed or returned via a method.
The rules for declaring parameters and how they are used is very much like those for C++ and VB. Methods can pass parameters by value or by reference and may declare them as optional, meaning that the parameter does not have to be supplied.
When passing a parameter by value, a copy of the data is sent to the method; when passing a parameter by reference, the address of the parameter is passed, which allows the method to change the data.
However, because you can't specify a default value in the traditional C++ sense, optional parameters are handled a little differently than in C++. Optional parameters must be passed as VARIANT data types, not as the actual data type they represent.
When using VB to access a method with optional parameters, VB will supply the parameter if one has not been provided. With C++, you are still required to supply a VARIANT parameter, even though it may not contain data.
As we stated at the beginning of the chapter, the sample Automation Server will be used to log strings of data to a file. The server will define the method OutputLines used by the user of the server to supply the string data that is written to the file. The method will accept an array of strings and an optional indentation parameter and will output the strings to the file. The indentation parameter is used to offset the strings by n number of tab characters to provide simple, yet effective, formatting to the data as it is output to the file.
Adding methods to an ATL project differs from the process for adding them
with MFC, which uses the ClassWizard that we are all familiar with. ATL uses a
custom ClassWizard that is accessed from the ClassView tab in the Project
Workspace window by right mouse clicking the interface that you are adding the
method to. From the ClassView tab in the Project Workspace window, select the
ITracker class from the list of ATLServer classes, click the
right mouse button, and select the Add Method menu item (see fig. 4.7).
FIG.
4.7
Add a new method to the server object.
In the Add Method to Interface dialog (see fig. 4.8), add the Method Name, OutputLines, and the Parameters, [in] VARIANT * varOutputArray, [in, optional] VARIANT varIndent, and [out, retval] VARIANT_BOOL * RetVal. Note that you add all of the parameters and their corresponding IDL attributes within this dialog. Table 4.2 contains a list of the parameter direction attributes that you add and their meanings. You are not required to modify the IDL after the method has been added.
Direction | Description |
in | Parameter is passed from caller to callee. |
out | Parameter is returned from callee to caller. |
in, out | Parameter is passed from caller to callee, and the callee returns a parameter. |
out, retval | Parameter is the return value of the method and is returned from the callee to the caller. |
NOTE: All optional parameters must be of type VARIANT, and they must fall at the end of the parameter list. Optional parameters are not managed in any way by OLE. It is the server application's responsibility to determine whether the VARIANT parameter passed to the method contains data and to either use the data passed to the method or convert the data to a useful type if possible, or to ignore the parameter if invalid data was passed and use the default value if appropriate, or to inform the user of an error condition if one of the above conditions was not met.
The ATL ClassWizard will add an entry to the IDL file (see Listing 4.6) for the
new method and will also add the function prototype (see Listing 4.7) and
implementation (see Listing 4.8) to the header and source file of the object
that the method is being added to. Note that the ClassWizard automatically added
the AFX_MANAGE_STATE macro to the implementation, which is required for
MFC support.
. . . [
object,
uuid(03699612-809E-11D0-BEFF-00400538977D),
dual,
helpstring("ITracker Interface"),
pointer_default(unique),
hidden
]
interface ITracker : IDispatch
{
[id(1), helpstring("method
OutputLines")] HRESULT OutputLines(
[in] VARIANT * varOutputArray,
[in,optional] VARIANT
[out,retval] VARIANT_BOOL * RetVal);
}; . . .
. . . // ISupportsErrorInfo
STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid); // ITracker
public:
STDMETHOD(OutputLines)(/*[in]*/ VARIANT * varOutputArray,
/*[in,optional]*/ VARIANT varIndent,
/*[out,retval]*/ VARIANT_BOOL *
RetVal); protected:
FILE * m_fileLog;
long m_lTimeBegin;
long
m_lHiResTime;
long m_lLastHiResTime; . . .
STDMETHODIMP CTracker::OutputLines(VARIANT *
varOutputArray, VARIANT varIndent,
VARIANT_BOOL * RetVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()) // TODO: Add your implementation
code here return S_OK; }
OutputLines is defined as having two parameters: varOutputArray, as a VARIANT passed by reference, that will contain a string array of data to output to the file; and varIndent, as a VARIANT passed by value, that is also an optional parameter indicating the amount of indentation when writing the string data to the file. The third parameter is actually the return type of the method and is defined as a VARIANT_BOOL.
See Chapter
3 regarding the use of Boolean data types and the differences between VB and
VC++.
NOTE: If you are converting an existing ODL file to IDL, be sure to change all Boolean declarations to VARIANT_BOOL. Boolean in ODL refers to a 2-byte signed short data type, whereas Boolean in IDL is a 1-byte unsigned char data type and is not compatible with VB. To further complicate matters VARIANT_BOOL is an unrecognized data type in ODL, though not in IDL.
Due to data type restrictions imposed by OLE Automation, you cannot pass
arrays as pa-rameters of methods. You can, however, pass VARIANT data
types that can contain arrays, thus the reason for defining
varOutputArray as a VARIANT. You are also required to pass
varOutputArray by reference because the array stored in the
VARIANT does not get copied over when it is passed by value.
Optional parameters must fall at the end of the parameter list and must be of type VARIANT. varIndent is an optional parameter that indents the text output as an added formatting feature.
The last step is to add the m_lIndent member variable to the class declaration, which is used in the OutputLines method implementation and later as a property of the server (see Listing 4.9).
. . . // ISupportsErrorInfo
STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid); // ITracker
public:
STDMETHOD(OutputLines)(/*[in]*/ VARIANT * varOutputArray, / *[in,optional]*/
VARIANT varIndent, /*[out,retval]*/ VARIANT_BOOL * RetVal); protected:
FILE
* m_fileLog;
long m_lTimeBegin;
long m_lHiResTime;
long
m_lLastHiResTime;
long m_lIndent; };
Before adding the OutputLines implementation, you need to update the constructor to initialize the m_lIndent member variable to a valid state (see Listing 4.10).
. . .
fprintf(m_fileLog, _T("Start %s\n"),
(LPCTSTR) oTimeStamp.Format("%B %#d, %Y, %I:%M %p"));
fprintf(m_fileLog,
_T("\n"));
}
m_lIndent = 0; }
Next you add the OutputLines implementation to the source file (see Listing 4.11). The implementation varies very little from the MFC sample. The only obvious difference is the return type of the function, which is now the last parameter of the function.
As with the MFC implementation, the ATL version checks the array parameter to ensure its validity and, if valid, outputs the data to the file, indenting the text if appropriate. See Chapter 3 for more information regarding the other implementation details.
For now the implementation returns VARIANT_FALSE in the cases where an error has occurred. Later in this chapter, you will learn how to create rich error information.
STDMETHODIMP CTracker::OutputLines(VARIANT *
varOutputArray, VARIANT varIndent,
VARIANT_BOOL * RetVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
HRESULT hResult = S_OK;
*RetVal = VARIANT_TRUE; // if we have a file a if the variant contains a string
array
if(m_fileLog && varOutputArray->vt == (VT_ARRAY | VT_BSTR))
{
// lock the array so we can use it
if(::SafeArrayLock(varOutputArray->parray) == S_OK)
{
LONG
lLBound;
// get the lower bound of the array
if(::SafeArrayGetLBound(varOutputArray->parray, 1, &lLBound) == S_OK)
{
LONG lUBound;
// get the number of elements in the array
if(::SafeArrayGetUBound(varOutputArray->parray, 1, &lUBound)
==
S_OK)
{
CString cstrIndent;
CTime oTimeStamp;
BSTR bstrTemp;
// if we have an indent parameter
if(varIndent.vt != VT_I4)
{
//
get a variant that we can use for conversion purposes VARIANT varConvertedValue;
// initialize the variant
::VariantInit(&varConvertedValue);
//
see if we can convert the data type to something useful
// -
VariantChangeTypeEx() could also be used
if(S_OK ==
:VariantChangeType(&varConvertedValue,
(VARIANT *) &varIndent, 0,
VT_I4))
// assign the value to our member variable
m_lIndent =
varConvertedValue.lVal;
}
else
// assign the value to our member
variable
m_lIndent = varIndent.lVal;
// if we have to indent the text
for(long lIndentCount = 0; lIndentCount < m_lIndent;
lIndentCount++)
// add a tab to the string
cstrIndent += _T("\t");
// for each of
the elements in the array
for(long lArrayCount = lLBound; lArrayCount <
(lUBound + lLBound); lArrayCount++)
{
// update the time
oTimeStamp = CTime::GetCurrentTime();
m_lHiResTime = timeGetTime();
// get the data from the array
if(::SafeArrayGetElement(varOutputArray->parray,
&lArrayCount,
&bstrTemp) == S_OK)
{
// output the data
fprintf(m_fileLog,
_T("%s(%10ld)-%s%ls\n"),
(LPCTSTR) oTimeStamp.Format ("%H:%M:%S"),
m_lHiResTime - m_lLastHiResTime,
(LPCTSTR) cstrIndent, bstrTemp);
//
store the last timer value
m_lLastHiResTime = m_lHiResTime;
// free the
bstr
::SysFreeString(bstrTemp);
}
}
}
else
*RetVal =
VARIANT_FALSE;
}
else
*RetVal = VARIANT_FALSE;
// unlock the
array we don't need it anymore
::SafeArrayUnlock(varOutputArray->parray);
}
else
*RetVal = VARIANT_FALSE;
}
else
*RetVal =
VARIANT_FALSE;
// return the result
return hResult; }
Now you have added a method. In the following section, you will learn how to implement its counterpart, the property.
A property can be thought of as an exposed variable that is defined in the Automation Server. Properties are useful for setting and retrieving information about the state of the server.
The m_lIndent member variable that you added to the class definition is a perfect candidate to be exposed as a property.
Properties are added in much the same way as methods. From the ClassView tab
in the Project Workspace window, select the ITracker class, click the
right mouse button, and select the Add Property menu item (see fig. 4.9).
FIG.
4.9
Add a new property to the Server with the ATL
ClassWizard.
In the Add Property to Interface dialog, set the Property Type to
long, type the Property Name as Indent, and leave the
remainder of the settings at their default values (see fig. 4.10). Click OK to
confirm the entry and close the dialog.
FIG.
4.10
Define the Indent property attributes.
Like the OutputLines method, the ATL ClassWizard added entries to the IDL file (see Listing 4.12), the Tracker.h header file (see Listing 4.13), and the Tracker.cpp source file (see Listing 4.14) to support the new property. As in Chapter 3, properties are added as a pair of related functions, and the same is true for the ATL server and ActiveX components.
. . . interface ITracker : IDispatch
{
[id(1), helpstring("method OutputLines")] HRESULT OutputLines(
[in]
VARIANT * varOutputArray, [in,optional] VARIANT
varIndent, [out,retval]
VARIANT_BOOL * RetVal);
[propget, id(2), helpstring("property Indent")]
HRESULT Indent(
[out, retval] long *pVal);
[propput, id(2),
helpstring("property Indent")] HRESULT Indent(
[in] long newVal);
}; . .
.
. . . // ITracker
public:
STDMETHOD(get_Indent)(/*[out, retval]*/ long *pVal);
STDMETHOD(put_Indent)(/*[in]*/ long newVal);
STDMETHOD(OutputLines)(/*[in]*/ VARIANT * varOutputArray,
/*[in,optional]*/ VARIANT varIndent, /*[out,retval]*/ VARIANT_BOOL *
RetVal); . . .
STDMETHODIMP CTracker::get_Indent(long * pVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()) // TODO: Add your
implementation code here return S_OK;
} STDMETHODIMP
CTracker::put_Indent(long newVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()) // TODO: Add your implementation
code here return S_OK; }
The actual implementation of the Indent property is very simple (see Listing 4.15). get_Indent returns the value currently stored in the member variable, and put_Indent stores the new value, after a little bit of error checking, in the member variable.
STDMETHODIMP CTracker::get_Indent(long * pVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()) HRESULT hResult = S_OK; //
return the member variable
*pVal = m_lIndent; // return the result
return hResult;
} STDMETHODIMP CTracker::put_Indent(long newVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()) HRESULT hResult = S_OK; // if
the new value is a least 0
if(newVal >= 0)
// assign the value to our
member variable
m_lIndent = newVal; // return the result
return hResult;
}
Properties, like methods, also have a wide variety of implementation options, including parameterized and enumerated values. See Chapters 6 through 11 on developing ActiveX controls for descriptions of more options and features when creating properties.
You've added methods and properties to the server, but you haven't really dealt with the issue of error handling in their implementation. In some cases, simply returning success or failure is not enough information for the developer to understand that an error occurred and what caused it. You will communicate more error information through the use of OLE exceptions.
While executing a method call or some other action, at times you will find it necessary to terminate the process due to some critical error that has occurred or is about to occur. For example, a method is called to write data to a file, but the method cannot open the file because there is not enough room on the hard disk to do so. You must halt further processing until the error can be resolved. An error of this kind is known as an exception. Any type of error can be treated as an exception; it depends upon the requirements of your application and how you choose to deal with the errors that may result.
You must become familiar with two forms of exceptions when creating ActiveX components. The first is a C++ exception. A C++ exception is a language mechanism used to generate critical errors of the type described earlier and is confined to the application in which they are defined. The second is an OLE exception. OLE exceptions are used to communicate the same kinds of errors externally to applications that are using a component. The difference between the two is that C++ exceptions are used internally to an application's implementation and OLE exceptions are used externally to other applications.
The COM implementation of a method in a dual-interface server does not have the same kind of error management features that IDispatch interfaces have. To generate the proper error information, an application must use the IErrorInfo object provided by the oper- ating system. A server need support only the ISupportErrorInfo interface, which lets an automation controller know that it should look at the IErrorInfo object for more infor-mation when an error occurs.
The first step is to add an enumeration of the types of errors that the server can generate to the IDL file (see Listing 4.16). This step has the effect of publishing the error constants to the user of the automation server. Unlike the ITracker interface, the enumeration can be added anywhere within the IDL file and still produce the proper C++ declaration in the ATLServer.h file. Remember to generate a new CLSID for the enumeration using the GUIDGEN.EXE program. See Chapter 2 for more information on how to use the GUIDGEN program.
. . . coclass CTracker
{
[default]
interface ITracker;
}; typedef [uuid(2B2AF9C9-5452-11D0-BEDE-00400538977D),
helpstring("Tracker Error Constants")]
enum tagTrackerError
{
MFCSERVER_E_NO_UBOUND = 46080,
MFCSERVER_E_NO_LBOUND = 46081,
MFCSERVER_E_NO_ARRAYLOCK = 46082,
MFCSERVER_E_NO_FILE = 46083,
MFCSERVER_E_BAD_ARRAY_PARAMETER = 46084,
MFCSERVER_E_INVALID_VALUE =
46085
}TRACKERERROR; };
Since ATL servers are dual-interface by default, you must implement all errors using OLE rich error information and not with C++ exceptions.
The next step is to add the actual error-generating code (see Listing 4.17). ATL provides the helper function AtlReportError for generating rich error information. The function accepts four parameters: the CLSID of the server, an error message, the IID of the interface, and the error code. Error codes must be formatted error codes, as in the implementation, or a predefined OLE error codes; simply returning S_FALSE is not enough to generate an error. See the VC++ books online for more information regarding OLE error codes and their use.
STDMETHODIMP CTracker::OutputLines(VARIANT *
varOutputArray,
VARIANT varIndent, VARIANT_BOOL * RetVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()) HRESULT hResult = S_OK; *RetVal
= VARIANT_TRUE; // if we have a file a if the variant contains a string array
if(m_fileLog && varOutputArray->vt == (VT_ARRAY | VT_BSTR))
{
// lock the array so we can use it
if(::SafeArrayLock(varOutputArray->parray) == S_OK)
{
LONG
lLBound; // get the lower bound of the array
if(::SafeArrayGetLBound(varOutputArray->parray, 1, &lLBound) == S_OK)
{
LONG lUBound; // get the number of elements in the array
if(::SafeArrayGetUBound(varOutputArray->parray, 1, &lUBound)
==
S_OK)
{
CString cstrIndent;
CTime oTimeStamp;
BSTR bstrTemp; //
if we have an indent parameter
if(varIndent.vt != VT_I4)
{
// get a
variant that we can use
// for conversion purposes
VARIANT
varConvertedValue; // initialize the variant
::VariantInit(&varConvertedValue); // see if we can convert the data
type to something
// useful - VariantChangeTypeEx() could also be used
if(S_OK == ::VariantChangeType (&varConvertedValue,
(VARIANT *)
&varIndent, 0, VT_I4))
// assign the value to our member variable
m_lIndent = varConvertedValue.lVal;
}
else
// assign the value
to our member variable m_lIndent = varIndent.lVal; // if we have to indent the
text
for(long lIndentCount = 0; lIndentCount < m_lIndent;
lIndentCount++)
// add a tab to the string
cstrIndent += _T("\t");
// for each of the elements in the array
for(long lArrayCount = lLBound;
lArrayCount <
(lUBound + lLBound); lArrayCount++)
{
// update the
time
oTimeStamp = CTime::GetCurrentTime();
m_lHiResTime = timeGetTime();
// get the data from the array
if(::SafeArrayGetElement
(varOutputArray->parray,
&lArrayCount, &bstrTemp) == S_OK)
{
// output the data
fprintf(m_fileLog, _T("%s(%10ld)-%s%ls\n"),
(LPCTSTR) oTimeStamp.Format ("%H:%M:%S"),
m_lHiResTime -
m_lLastHiResTime,
(LPCTSTR) cstrIndent, bstrTemp); // store the last timer
value
m_lLastHiResTime = m_lHiResTime; // free the bstr
::SysFreeString(bstrTemp);
}
}
}
else
{
*RetVal =
VARIANT_FALSE; // create the error message
hResult =
AtlReportError(CLSID_Tracker,
"Unable to retrieve the upper bound dimension
of the array.",
IID_ITracker,
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF,
MFCSERVER_E_NO_UBOUND));
}
}
else
{
*RetVal =
VARIANT_FALSE; // create the error message
hResult =
AtlReportError(CLSID_Tracker,
"Unable to retrieve the lower bound dimension
of the array.",
IID_ITracker,
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF,
MFCSERVER_E_NO_LBOUND));
} // unlock the array we don't need it anymore
::SafeArrayUnlock(varOutputArray->parray);
}
else
{
*RetVal = VARIANT_FALSE; // create the error message
hResult =
AtlReportError(CLSID_Tracker,
"Unable to lock the array memory.",
IID_ITracker,
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF,
MFCSERVER_E_NO_ARRAYLOCK));
}
}
else
{
*RetVal =
VARIANT_FALSE; // if there wasn't a file
if(!m_fileLog)
// create the
error message
hResult = AtlReportError(CLSID_Tracker,
"Invalid File
Handle. File could not be opened for output.",
IID_ITracker,
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_FILE));
else
// create the error message
hResult = AtlReportError(CLSID_Tracker,
"The first parameter must be a string array passed by reference.",
IID_ITracker,
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF,
MFCSERVER_E_BAD_ARRAY_PARAMETER));
} // return the result
return
hResult;
} STDMETHODIMP CTracker::get_Indent(long * pVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()) HRESULT hResult = S_OK; //
return the member variable
*pVal = m_lIndent; // return the result
return hResult;
} STDMETHODIMP CTracker::put_Indent(long newVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()) HRESULT hResult = S_OK; // if
the new value is a least 0
if(newVal >= 0)
// assign the value to our
member variable
m_lIndent = newVal;
else
{
// create the error
message
hResult = AtlReportError(CLSID_Tracker,
"Invalid value. Value
must be 0 or greater.",
IID_ITracker,
MAKE_SCODE(SEVERITY_ERROR,
FACILITY_ITF, MFCSERVER_E_INVALID_VALUE));
} // return the result
return
hResult; }
The use of C++ exceptions is still permitted with an ATL-implemented server. But the exception cannot cross application boundaries, which is the case in any application whether implemented in MFC, ATL, or some other framework.
In Chapter 3, the basic MFC server is implemented as IDispatch only. You are required to add dual-interface support as an extra step. With the ATL, dual-interface support is built-in and implemented as a normal aspect of the server.
Again, as in Chapter 3, the basic MFC server is implemented as IDispatch only, and it is possible to throw standard C++ exceptions and have the basic MFC IDispatch support code translate the error into an OLE exception. When an MFC server is converted to dual-interface, you must implement the exception translation code yourself.
For ATL, the server has been implemented as dual-interface from the start, and all error generation has been written as true OLE exceptions and does not require translation.
OLE is not the only method for creating and using Automation Servers. This chapter will show you how to create OLE servers using C++ syntax.
At times, you must create and use Automation Servers from within the application in which they are defined. Take, for example, a case where an application contains three servers, with only one being directly creatable by outside applications using OLE. The remaining two servers can be created by the exposed server using C++ and returned via a method call to another application, which then uses the server as though it was created via OLE.
For an MFC server, the inclusion or exclusion of the macros DECLARE_OLECREATE and IMPLEMENT_OLECREATE determines whether a server is creatable by external applications. For ATL, it is a little simpler. All ATL applications contain a global variable called an ObjectMap for declaring all of the servers that can be created via OLE. The ObjectMap is declared in the main application file (see the file ATLServer.cpp) as a pair of macros:
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
Each OLE server implemented within the application will have a
single entry within the body of the ObjectMap macro, thus identifying
the server as an exposed OLE server. The OBJECT_ENTRY macro defines the
CLSID and the C++ class of the server that can be created.
BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_Tracker, CTracker) END_OBJECT_MAP()
To prevent an application from being exposed as an Automation Server, you just remove or comment out the entry in the object map. When adding additional servers to an application, you must add an entry for each new server to the ObjectMap macro.
All ATL servers contain a static function CreateInstance, which is used to instantiate instances of themselves. You must use only the CreateInstance function to instantiate a server since it is implemented by the class factory of the server and will manage the server instantiation correctly. This is very critical in the cases where the server is shared among two or more applications, as you will see in the following sections.
The first step is to declare the pointer to which you will store the reference of the object when it is created. Because ATL servers are implemented using templates, it may seem a little strange to declare the reference this way. However, this makes sense when you see the architecture of ATL, which is documented fully in the VC++ books online.
CComObject<CTracker> * opTracker;
The next step is to instantiate the server and store the reference of the new object. Remember to check the return value of the function and the pointer to ensure that the object was instantiated successfully.
CComObject<CTracker>::CreateInstance(&opTracker);
Once a server is instantiated this way, you can use it like any other C++ class or OLE server. You can use QueryInterface to retrieve IDispatch or custom interface pointers that can be passed to other applications. Refer to Chapter 3 for more information regarding the instantiation and use of OLE servers with C++.
So far, you've only looked at how to create individual instances of objects. In the following section, you will learn how to share objects.
OLE defines a facility for sharing objects called the Running Object Table. Essentially, a shareable object will publish its CLSID and an IUnknown reference to itself in the Running Object Table. Any application that so desires can ask for the running instance of the object rather than create a new instance. Applications that may need to work with a single running instance of an application may find it more useful to use shared objects than to create multiple copies. The Tracker object is a perfect candidate for this kind of functionality. Multiple applications could use the same Tracker object to log information, thus saving on memory.
Unfortunately, the way ATL is implemented prevents you from adding shared object support without actually creating new ATL template classes. This limitation occurs because of dependence on the Release function implementation to revoke the object from the Running Object Table, which you cannot override directly in the base ATL classes.
Listing 4.18 shows the support code that has been added to the StdAfx.h file to support shared objects. The new classes and macros are based on the original ATL code and have been extended to register the server in the Running Object Table. The code will also remove the server from the Running Object Table when the reference count reaches 1.
The only real change made to the original ATL code is that a new class CComObjectShared is added with an extra template parameter of the CLSID of the server. The remaining changes to the code and macros are to reflect the use of the new class versus its original implementation CComObject. Do note that the shared server implementation is simple and does not support aggregatable objects. But that is not to say it cannot be implemented; it just wasn't done for this sample.
The constructor of the CComObjectShared class adds the IUnknown reference to the Running Object Table and stores the ID in a member variable to be used later when revoking the server.
The Release function is implemented the same as the MFC sample in that the Release implementation revokes the server from the Running Object Table. The code must also protect the Release call by bumping up the reference count of the server and clearing the member variable to prevent recursion.
// ****** ATL 2.0 version - Added by Jerry
Anderson for shared object support
// **
#define
DECLARE_NOT_AGGREGATABLE_SHARED(cBase, clsid) public:\
typedef
CComCreator2< CComCreator< CComObjectShared<cBase, clsid> >,
CComFailCreator<CLASS_E_NOAGGREGATION> > _CreatorClass; // if this
object was registered and the refcount is 1 (which is from the
"RegisterActiveObject")
// then revoke the registration so the object can be
destroyed properly - The AddRef/Release pair
// is to protect the
destruction and prevent the object from being deleted before we are out of this
call
// since the RevokeActiveObject is going to call "Release" also and the
refcount would be 0 if we didn't AddRef #define RELEASE_AND_DESTROY_SHARED() \
InternalRelease(); \
if(dwRegister && m_dwRef == 1) \
{
InternalAddRef(); DWORD dwtRegID = dwRegister; dwRegister = 0;
::RevokeActiveObject(dwtRegID, NULL); InternalRelease(); } \
if(m_dwRef ==
0) \
{ delete this; return 0; } \
return m_dwRef //Base is the user's
class that derives from CComObjectRoot and whatever
//interfaces the user
wants to support on the object
template <class cBase, const CLSID *
clsid>
class CComObjectShared : public cBase
{
public:
//
this is here to prevent an ASSERT when executing
"InternalFinalConstructRelease()"
DECLARE_PROTECT_FINAL_CONSTRUCT();
ULONG dwRegister;
typedef cBase _BaseClass;
CComObjectShared(void* =
NULL)
{
// protect the construction
this->InternalAddRef(); //
lock down the application so it does not fall out from under us
_Module.Lock(); // clear the member
dwRegister = NULL; // Initialize an
IUnknown reference
LPUNKNOWN pIUnknown = NULL;
// QI for the
IUnknown
if(_InternalQueryInterface(IID_IUnknown, (void **) &pIUnknown)
== S_OK)
{
// register the clsid as an active object so other
applications will get the same object
if(::RegisterActiveObject(pIUnknown,
*clsid, ACTIVEOBJECT_STRONG, &dwRegister) != S_OK)
// clear the member
dwRegister = NULL; // release the IUnknown
pIUnknown->Release();
} // protect the construction
this->InternalRelease();
}
virtual ~CComObjectShared()
{
// Set refcount to 1 to protect
destruction
m_dwRef = 1L;
FinalRelease();
_Module.Unlock();
}
//If InternalAddRef or InteralRelease is undefined then your class
//doesn't
derive from CComObjectRoot
STDMETHOD_(ULONG, AddRef)()
{
// release
the IUnknown reference
return InternalAddRef();
}
STDMETHOD_(ULONG,
Release)()
{
RELEASE_AND_DESTROY_SHARED();
}
//if
_InternalQueryInterface is undefined then you forgot BEGIN_COM_MAP
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{return
_InternalQueryInterface(iid, ppvObject);}
static HRESULT WINAPI
CreateInstance(CComObjectShared<cBase, clsid>**pp);
}; // needed for
ATL version 1.1
// template <class cBase, const CLSID * clsid>
//
CComObjectShared<cBase, clsid>* CComObjectShared<cBase,
clsid>::pSharedObject = NULL;
template <class cBase, const CLSID *
clsid>
HRESULT WINAPI CComObjectShared<cBase,
clsid>::CreateInstance(CComObjectShared<cBase, clsid>** pp)
{
_ASSERTE(pp != NULL);
HRESULT hRes = E_OUTOFMEMORY;
CComObjectShared<cBase, clsid>* p = NULL;
ATLTRY((p = new
CComObjectShared<cBase, clsid>()))
if (p != NULL)
{
p->SetVoid(NULL);
p->InternalFinalConstructAddRef();
hRes =
p->FinalConstruct();
// this line differs from the original code -
//
for some reason the reference counts
// for the object are not correct when
created this way
p->InternalAddRef();
p->InternalFinalConstructRelease();
if (hRes != S_OK)
{
delete p;
p = NULL;
}
}
*pp = p; return hRes;
} // **
// ****** ATL 2.0 version - Added by Jerry Anderson for shared object
support
As the server developer, the only thing you must do is add the macro DECLARE_NOT_AGGREGATABLE_SHARED(...) to the ATL server class. Listing 4.19 shows the change that was made to the CTracker sample to enable shared server support.
. . . DECLARE_REGISTRY_RESOURCEID(IDR_TRACKER)
BEGIN_COM_MAP(CTracker)
COM_INTERFACE_ENTRY(ITracker)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP() DECLARE_NOT_AGGREGATABLE_SHARED(CTracker, &CLSID_Tracker)
// ISupportsErrorInfo
STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid); //
ITracker public:. . .
During the lifetime of the server, you can get the same instance of the server and use it from multiple applications. In VB, getting the running instance of a server is done with the GetObject call and in VC++ with the GetActiveObject function. After the pointer to the server is retrieved, the server can be used as though it was created through normal OLE mechanisms.
This method of sharing objects is fine but requires that the application using the server take an active role in deciding to use the shared object versus creating its own instance of the object. Another approach is to supply the instance of a running server to an application that calls CreateObject rather than GetObject. This approach is known as a single instance server.
To support single instance servers, you must perform all of the steps described in the section entitled "Shared Servers," earlier in this chapter, from within the ClassFactory of the server, not from within the implementation of the server itself. By implementing the object sharing code within the class factory, you are able to control the number of instances of the server without having to rely on the user of the server to program specifically for those cases.
As is the case with the shared server support, we have created a new set of classes and macros for creating a server consisting of a single instance (see Listing 4.20). The classes are based on the existing ATL classes and differ slightly from the original implementation. In this case, two new classes were created: CComCreatorSingle and CComObjectSingle.
CComCreatorSingle is the class that is responsible for creating the server--if one has not already been created--or retrieving the existing server and returning it instead of creating a new instance. CComObjectSingle is also responsible for registering the server in the Running Object Table. As with the shared server implementation, CComObjectSingle removes itself from the Running Object Table, thereby protecting itself from recursive entry into the Release function. The CComObjectSingle also supports a CreateInstance function, which is used to create copies of the server using C++ syntax. CComObjectSingle::CreateInstance must also contain support to create a new server if one is not already running or use the existing server if available.
// ****** ATL 2.0 version - Added by Jerry
Anderson for single instance object support
// **
#define
DECLARE_NOT_AGGREGATABLE_SINGLE(cBase, clsid, iBase, iid) public: \
typedef
CComCreator2< CComCreatorSingle< CComObjectSingle<cBase, clsid, iBase,
\
iid>, clsid>, CComFailCreator<CLASS_E_NOAGGREGATION> >
CreatorClass; // if this object was registered and the refcount is 1 (which is
from the
// "RegisterActiveObject")
// then revoke the registration so
the object can be destroyed properly - The
// AddRef/Release pair
// is
to protect the destruction and prevent the object from being deleted before we
are
// out of this call
// since the RevokeActiveObject is going to call
"Release" also and the refcount would // be 0 if we didn't AddRef #define
RELEASE_AND_DESTROY_SINGLE() \
InternalRelease(); \
if(dwRegister
&& m_dwRef == 1) \
{ InternalAddRef(); DWORD dwtRegID = dwRegister;
dwRegister = 0; ::RevokeActiveObject(dwtRegID, NULL); InternalRelease(); } \
if(m_dwRef == 0) \
{ delete this; return 0; } \
return m_dwRef //
Base is the user's class that derives from CComObjectRoot and whatever
//
interfaces the user wants to support on the object
template <class cBase,
const CLSID * clsid, class iBase, const IID * iid>
class CComObjectSingle
: public cBase
{
public:
// this is here to prevent an ASSERT when
executing "InternalFinalConstructRelease()"
DECLARE_PROTECT_FINAL_CONSTRUCT();
ULONG dwRegister;
typedef cBase
_BaseClass;
CComObjectSingle(void* = NULL)
{
// protect the
construction
this->InternalAddRef(); // lock down the application so it
does not fall out from under us
_Module.Lock(); // clear the member
dwRegister = NULL; // Initialize an IUnknown reference
LPUNKNOWN
pIUnknown = NULL;
// se if the object is already running
::GetActiveObject(*clsid, NULL, &pIUnknown); // if we didn't get a
reference to a running object
if(!pIUnknown)
{
// QI for the
IUnknown
if(_InternalQueryInterface(IID_IUnknown, (void **) &pIUnknown)
== S_OK)
{
// register the clsid as an active object so other
applications
// will get the same object
::RegisterActiveObject(pIUnknown, *clsid, ACTIVEOBJECT_STRONG,
&dwRegister); // release the IUnknown
pIUnknown->Release();
}
// clear the reference just to be safe
pIUnknown = NULL;
}
else
// release the IUnknown
pIUnknown->Release(); // protect the
construction
this->InternalRelease();
}
virtual
~CComObjectSingle()
{
// Set refcount to 1 to protect destruction
m_dwRef = 1L;
FinalRelease();
_Module.Unlock();
} //If
InternalAddRef or InteralRelease is undefined then your class
//doesn't
derive from CComObjectRoot
STDMETHOD_(ULONG, AddRef)()
{
// release
the IUnknown reference
return InternalAddRef();
}
STDMETHOD_(ULONG,
Release)()
{
RELEASE_AND_DESTROY_SINGLE();
}
//if
_InternalQueryInterface is undefined then you forgot BEGIN_COM_MAP
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{return
_InternalQueryInterface(iid, ppvObject);}
static HRESULT WINAPI
CreateInstance(CComObjectSingle<cBase, clsid, iBase,
iid>** pp);
}; // needed for ATL version 1.1
// template <class cBase, const
CLSID * clsid, class iBase, const IID * iid>
//
CComObjectSingle<cBase, clsid, iBase, iid>* CComObjectSingle<cBase,
clsid, iBase,
// iid>::pSingleObject = NULL; template <class cBase,
const CLSID * clsid, class iBase, const IID * iid>
HRESULT WINAPI
CComObjectSingle<cBase, clsid, iBase,
iid>::CreateInstance(CComObjectSingle<cBase, clsid, iBase, iid>** pp)
{
_ASSERTE(pp != NULL);
HRESULT hRes = E_OUTOFMEMORY; // Initialize
an IUnknown reference
LPUNKNOWN pIUnknown = NULL;
// se if the
object is already running
::GetActiveObject(*clsid, NULL, &pIUnknown);
// if we didn't get a reference to a running object
if(!pIUnknown)
{
CComObjectSingle<cBase, clsid, iBase, iid>* p = NULL;
ATLTRY((p =
new CComObjectSingle<cBase, clsid, iBase, iid>()))
if (p != NULL)
{
p->SetVoid(NULL);
p->InternalFinalConstructAddRef();
hRes = p->FinalConstruct();
// this line differs from the original
code -
// for some reason the reference counts
// for the object are not
correct when created this way
p->InternalAddRef();
p->InternalFinalConstructRelease();
if (hRes != S_OK)
{
delete p;
p = NULL;
}
}
*pp = p;
}
else
{
//
get a pointer
iBase * piBase = NULL; // QI for the interface
pIUnknown->QueryInterface(*iid, (LPVOID*) &piBase); // cast the
interface pointer to the class
*pp = (CComObjectSingle<cBase, clsid,
iBase, iid>*) piBase; // release the IUnknown reference
pIUnknown->Release();
} return hRes;
} template <class T1,
const CLSID* clsid>
class CComCreatorSingle
{
public:
static
HRESULT PASCAL CreateInstance(void* pv, REFIID riid, LPVOID* ppv)
{
_ASSERTE(*ppv == NULL);
HRESULT hRes = E_OUTOFMEMORY; // Initialize an
IUnknown reference
LPUNKNOWN pIUnknown = NULL;
// se if the object
is already running
::GetActiveObject(*clsid, NULL, &pIUnknown); // if we
didn't get a reference to a running object
if(!pIUnknown)
{
T1* p =
NULL;
ATLTRY(p = new T1(pv))
if (p != NULL)
{
p->SetVoid(pv);
p->InternalFinalConstructAddRef();
hRes = p->FinalConstruct();
p->InternalFinalConstructRelease();
if (hRes == S_OK)
hRes =
p->QueryInterface(riid, ppv);
if (hRes != S_OK)
delete p;
}
}
else
{
// get the IID that was requested
hRes =
pIUnknown->QueryInterface(riid, ppv); // release the IUnknown reference
pIUnknown->Release();
}
return hRes;
}
}; // **
//
****** ATL 2.0 version - Added by Jerry Anderson for single instance object
support
As with shared server support, you must add a new macro to your class definition to enable your server for single instance support. Add the macro DECLARE_NOT_AGGREGATABLE_SINGLE to the CTracker class definition supplying the class name, CLSID, interface name, and IID of your server. Listing 4.21 shows the CTracker implementation of single instance server support.
. . . DECLARE_REGISTRY_RESOURCEID(IDR_TRACKER)
BEGIN_COM_MAP(CTracker)
COM_INTERFACE_ENTRY(ITracker)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP() // DECLARE_NOT_AGGREGATABLE_SHARED(CTracker,
&CLSID_Tracker)
DECLARE_NOT_AGGREGATABLE_SINGLE(CTracker,
&CLSID_Tracker, ITracker, &IID_ITracker) // ISupportsErrorInfo
STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid); // ITracker . .
.
Shared server support is very straightforward to implement and use and adds a level of functionality not normally available to standard server implementations.
In this chapter, you created a basic implementation of an Automation Server. You also expanded upon the basic framework provided by ATL to create new and interesting features within your implementation. ATL provides a clean and easy way to implement Automation Servers. Combined with MFC or STL (Standard Template Library), ATL is a powerful platform for creating ActiveX components. ATL has the added benefit of being a product supported by Microsoft, which is not the case with the BaseCtl framework.
Like MFC, ATL servers can benefit from the addition of User Interface and Events. The creation of services and remote servers also makes the prospect of implementing ActiveX servers enticing.
Automation Servers provide a flexible way to create lightweight ActiveX components for use by your applications. The support of both IDispatch interfaces and custom interfaces (a dual-interface server) also gives the user of the server a lot of flexibility in terms of implementation styles and methods.
The next chapter looks at creating an Automation Server using the BaseCtl framework.