Полезная информация

Chapter 4
Creating ActiveX Automation Servers Using ATL


Creating ActiveX Automation Servers Using ATL

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.

Creating the Basic Project

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.
Table 4.1 Basic Source Files Created by the ATL AppWizard
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.

Adding an Automation Interface to the Application

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.

Listing 4.1 ATLSERVER.IDL--IDL File for the ATL Server Sample

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.

Registry

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.

Listing 4.2 ATLCONTROLWIN.RGS--Sample Registry Script File for the CTracker Server Object

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.

Listing 4.3 TRACKER.RGS--Updated Tracker.rgs File

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%'
}
} }

Sample Server Support Code

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.

Listing 4.4 Tracker.H--Sample Server Support Code Added to the Tracker Header File

/////////////////////////////////////////////////////////////////////////////
// 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.

Listing 4.5 TRACKER.CPP--Sample Server Support Code Added to the Source File


// 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.

Adding Methods

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.
Table 4.2 Parameter Flow Attributes
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.
The Attributes button displays a dialog for adding IDL attributes for the entire function declaration. Click OK to close the dialog, and add the method to your implementation.

FIG. 4.8

Define the OutputLines method.

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.

Listing 4.6 ATLSERVER.IDL--OutputLines Method Added to the IDL File by the ATL ClassWizard


. . . [
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);
}; . . .

Listing 4.7 TRACKER.H--OutputLines Function Prototype Added to the Tracker.h File


. . . // 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; . . .

Listing 4.8 TRACKER.CPP--OutputLines Function Implementation Added to the Tracker.cpp File


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).

Listing 4.9 TRACKER.H--m_1Indent Member Variable Added to the Class Definition


. . . // 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).

Listing 4.10 TRACKER.CPP--Member Initialization in the Constructor

. . .
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.

Listing 4.11 TRACKER.CPP--OutputLines Function Implementation Added to the Source File Tracker.cpp


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.

Adding Properties

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.

Listing 4.12 TRACKER.IDL--Indent Property Added to the IDL


. . . 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);
}; . . .

Listing 4.13 TRACKER.H--Indent Property Function Pair Prototypes Added to the Tracker.h File


. . . // 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); . . .

Listing 4.14 TRACKER.CPP--Indent Property Function Pair Implementation Added to the Tracker.cpp File


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.

Listing 4.15 TRACKER.CPP--Indent Property Implementation


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.

Generating 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.

Listing 4.16 ATLSERVER.IDL--Error Enumeration Added to the IDL File


. . . 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.

Listing 4.17 TRACKER.CPP--Rich Error Information Added to Tracker Implementation


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.

Dual-Interface

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.

Generating Dual-Interface OLE Exceptions

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.

Server Instantiation Using C++

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.

Shared Servers

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.

Listing 4.18 STDAFX.H--Shared Object Support Classes and Macros


// ****** 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.

Listing 4.19 TRACKER.H--Shared Server Support Added to Ctracker Class


. . . 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.

Single Instance Servers

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.

Listing 4.20 STDAFX.G--Single Instance Server Support Added to StdAfx.h


// ****** 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.

Listing 4.21 TRACKER.H--Single Instance Server Support Added to CTracker Class


. . . 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.

From Here...

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.