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

Chapter 3


Creating ActiveX Automation Servers Using MFC

MFC and Visual C++ (VC++) provide a very simple and easy to use framework for creating ActiveX Automation Servers. In fact, the VC++ development environment's AppWizard and ClassWizard are implemented with this in mind. Creating and manipulating automation interfaces is one of VC++'s primary functions.

In this chapter, you will create a simple in-process Automation Server using MFC for logging string data to a file. Throughout this chapter, you can use an application such as Visual Basic (VB) to test your implementation. VB is perfect for accessing Automation Servers since it takes so little time and code to do so.

As you proceed through the chapter, you will expand on your implementation, highlighting some of the more advanced concepts of Automation Server creation.

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. MFC provides an AppWizard that greatly simplifies this process. The AppWizard consists of a set of structured dialogs and choices that, in the end, will result in a set of files representing the basic application's project.

To create the basic project, you need to open the VC++ Integrated Development Environment (IDE) and from the File menu, select the New menu item. Select the Projects tab in the New dialog, and select MFC AppWizard (dll) as the type. Enter the Project name MFCServer, and set the Location to the \Que\ActiveX\MFCServer directory (see fig. 3.1). Click OK to continue.

FIG. 3.1
Select the application type, name, and location of your new project.

In the MFC AppWizard -- Step 1 of 1 dialog, you define the specifics about how your application is going to be created (see fig. 3.2). For the type of DLL to create, select Regular DLL with MFC statically linked, which results in a slightly larger application but one that should load faster because you won't have to load the MFC DLLs whenever the server is launched. Also, check the Automation check box--since that is the reason you are creating the application in the first place. Click the Finish button to continue.

FIG. 3.2
Define the specific application features in the MFC AppWizard -- Step 1 of 1 dialog.

The New Project Information dialog allows you to review your choices before creating the actual project (see fig. 3.3). Click OK to complete the creation of your project.

FIG. 3.3
Confirm the project settings in the New Project Information dialog.

The MFC AppWizard will create all the basic files that are needed to create a DLL-based Automation Server. Table 3.1 lists all of the files that are created for you and a brief explanation of what they are used for.
Table 3.1 Basic Source Files Created by the MFC AppWizard
Filename Description
MFCServer.clw VC++ project file.
MFCServer.cpp Main application source file and entry point for the DLL.
MFCServer.def Standard application DEF file. This file contains the function export declarations needed for all in-process servers.
MFCServer.dsp VC++ project file.
MFCServer.dsw VC++ project file.
MFCServer.h Main application header file.
MFCServer.ncb VC++ project file.
MFCServer.odl Standard Object Definition Language (ODL) file.
MFCServer.rc Standard resource file.
ReadMe.txt Text file that describes the project.
Resource.h Resource header file.
StdAfx.cpp Standard precompiled header source file.
StdAfx.h Standard precompiled header file. All the MFC-specific include files are added here.
Res\MFCServer.rc2 Standard resource 2 file. This file contains all of the resource information that cannot be edited directly by VC++.
At this point, you can compile your project, but you can do very little with it since it does not contain interfaces, methods, or properties.

Adding an Automation Interface to the Application

To be an Automation Server, an application must contain at least one or more IDispatch-based interfaces. In MFC, the CCmdTarget class is used to implement this interface. You will use the MFCClassWizard to add your automation interfaces to your application. From the View menu, select the ClassWizard menu item. Click the Add Class button, and select the New menu item to open the New Class dialog (see fig. 3.4).

FIG. 3.4
Add a new Automation Server class using the Add C
lass feature of the MFC ClassWizard.

Enter the Name CTracker, and select CCmdTarget as its base class in the Base class combo box. Select the Automation radio button in the Automation radio button group. The Createable by type ID radio button and edit field are used to define the ProgID that will be used to create and launch the Automation Server. The human readable ProgID is used in place of the CLSID since it is much easier to write and remember. Be careful when defining a ProgID not to create duplicates. For your application, leave the ProgID set to its default value. Click OK to create your new class and add it to your application. Click OK in the MFC ClassWizard dialog to close the ClassWizard.

When creating a new CCmdTarget class, MFC not only creates a header and source file with all of the appropriate information, Tracker.h and Tracker.cpp in your case, but since you selected automation support, it also updates the ODL file with the new Dispinterface and CoClass entries (see Listing 3.1) of your Automation Server.

The Dispinterface is your primary IDispatch-based interface and is where the ClassWizard will add your new methods and properties. The CoClass interface identifies your class factory interface. The class factory is the part of the application that performs the actual creation of your Automation Server when it is necessary to do so. See the OLE and MFC documentation for more information on class factories and their role in OLE.

Listing 3.1 MFCSERVER.ODL--Dispinterface and CoClass ODL Entries

// MFCServer.odl : type library source for MFCServer.dll
// This file will be processed by the Make Type Library (mktyplib) tool to
// produce the type library (MFCServer.tlb).
[ uuid(11C82943-4EDD-11D0-BED8-00400538977D), version(1.0) ]
library MFCServer
{
importlib("stdole32.tlb");
// Primary dispatch interface for CTracker
[ uuid(11C82946-4EDD-11D0-BED8-00400538977D) ]
dispinterface ITracker
{
properties:
// NOTE - ClassWizard will maintain property information here.
// Use extreme caution when editing this section.
//{{AFX_ODL_PROP(CTracker)
//}}AFX_ODL_PROP
methods:
// NOTE - ClassWizard will maintain method information here.
// Use extreme caution when editing this section.
//{{AFX_ODL_METHOD(CTracker)
//}}AFX_ODL_METHOD
};
// Class information for CTracker
[ uuid(11C82947-4EDD-11D0-BED8-00400538977D) ]
coclass TRACKER
{
[default] dispinterface ITracker;
};
//{{AFX_APPEND_ODL}}
};

Even though the MFC ClassWizard added the new interface to the server, it did not expose the interface to the outside world. Basically, what you have right now is an Automation Server that cannot be created by any application. Not very useful.

All ActiveX components are created through an object known as a class factory. MFC defines the class COleObjectFactory for its class factory support. You do not add the COleObjectFactory class directly to your server implementation; instead, you need to use two macros defined by MFC: DECLARE_OLECREATE and IMPLEMENT_OLECREATE.

In the Project Workspace, select the ClassView tab. Expand the class list, and double-click the CTracker class to open the Tracker.h file. Add the macro DECLARE_OLECREATE to your class definition as in Listing 3.2. The macro takes a single parameter, CTracker, which is your class name.

Listing 3.2 TRACKER.H--Add the Class Factory Support with the Macro DECLARE_OLECREATE

. . .
// NOTE - the ClassWizard will add and remove member functions here.
//}}AFX_DISPATCH
DECLARE_DISPATCH_MAP()
DECLARE_INTERFACE_MAP()
DECLARE_OLECREATE(CTracker)
};

Next in the Project Workspace window, select the FileView tab, expand the Source Files list, and double-click the Tracker.cpp entry to open the file. Add the macro IMPLEMENT_OLECREATE to your source file (see Listing 3.3). The IMPLEMENT_OLECREATE macro takes three parameters: the class name, the ProgID that is used to create the server, and the CLSID of the CoClass interface, as defined in your ODL file. When the ODL file was created by the AppWizard, a CLSID was generated for the type library. When the ClassWizard added the CTracker class, it too created new CLSIDs; one for the Dispinterface and the other for the CoClass.

Listing 3.3 TRACKER.CPP--Add the Class Factory Implementationwith the IMPLEMENT_OLECREATE Macro

. . .
// {11C82946-4EDD-11D0-BED8-00400538977D}
static const IID IID_ITracker =
{ 0x11c82946, 0x4edd, 0x11d0, { 0xbe, 0xd8, 0x0, 0x40, 0x5, 0x38, 0x97, 0x7d } };
BEGIN_INTERFACE_MAP(CTracker, CCmdTarget)
INTERFACE_PART(CTracker, IID_ITracker, Dispatch)
END_INTERFACE_MAP()
IMPLEMENT_OLECREATE(CTracker, _T("MFCServer.Tracker"), 0x11C82947, 0x4edd, 0x11d0, 0xbe, 0xd8, 0x0, 0x40, 0x5, 0x38, 0x97, 0x7d)
. . .

Your server implementation is now class factory enabled, which allows it to be created by other applications. Before another application can create 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.

Server Registration

Local servers rely on command-line options for registration support. It is the responsibility of the local server developer to check for the correct command-line option and take the appropriate action.
Table 3.2 Local Server Command-Line Options for Registration Support
Command-Line Option Description
R Register all components.
U Unregister all components.
S Perform registration in silent mode and do not display confirmation dialogs, indicating success. Error messages should still be displayed. This option can be combined with R or U.


All inproc ActiveX components expose registration support via two exported functions: DllRegisterServer and DllUnregisterServer.

The MFC AppWizard will automatically add the DllRegisterServer function to the main application file of a project when it is created. The registration of all of the components contained in the application should be performed in this function, with each ActiveX component being responsible for its own registration support.

Registration support is handled automatically by the COleObjectFactory class. Even though you may not be aware of it, the COleObjectFactory class contains a singly linked list that is used to keep track of all of the COleObjectFactory classes implemented in a single application. The linked list is a static member, which means that all instances of the class share the same class factory list. COleObjectFactory also contains a static function, UpdateRegistryAll, that will cycle through the list of COleObjectFactory classes, instructing each to register themselves.

Server Unregistration

The MFC AppWizard does not add the exported function, DllUnregisterServer, to a project when it is created. This is probably due to an inherent limitation in MFC. The MFC group apparently did not feel it was necessary to add unregistration support to the basic MFC COleObjectFactory class. This is very interesting since all of the Microsoft logo requirements indicate that all applications that are installed and registered must also uninstall and unregister themselves in order to qualify for the logo.

To support server unregistration, you would have to add the exported function, DllUnregisterServer and call the static function COleObjectFactory::UpdateRegistryAll passing FALSE as the parameter.

The actual unregistration code requires more work. We didn't include the unregistration code as a part of the sample code, but the implementation is straightforward and is outlined here. The first step is to create a new class that inherits from COleObjectFactory, and override the virtual function, UpdateRegistry. Check the parameter that is passed to the function, and based on its value, call the appropriate registration and unregistration code. MFC provides a basic registration helper function, AfxOleRegisterServerClass, but does not define a companion helper function for unregistration. Searching the source files in MFC reveals a complete set of helper functions for the registry, but unfortunately they are not accessible from anything but an MFC-implemented ActiveX control. Since nothing is available from MFC, you are required to implement the registry updating code yourself. Remember to remove all of the registry entries that the server created: the ProgID, the CLSID, and the type library.

Sample Server Support Code

Since your sample server is used to output data to a file, you first need to add some support code to your application before adding its methods and properties.

Listing 3.4 shows the changes and additions that have been made to the class header file. A set of member variables was added for storing the file handle and timer information that will be used throughout the server implementation.

Listing 3.4 TRACKER.H-- Sample Server Support Code Added to the Header File

. . .
DECLARE_OLECREATE(CTracker)
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 to the Tracker.cpp file (see Listing 3.5). This is for the timer functions that you are taking advantage of in the sample server implementation.

The constructor and destructor of the server have also been updated. When using OLE in MFC applications, you must always lock the application in memory by calling the method AfxOleLockApp(), which ensures that the application will not be unloaded from memory until the reference count reaches zero. This step is critical and must be in all MFC-based servers.

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 3.5 TRACKER.CPP--Updated Source File

. . .
#include "Tracker.h"
// needed for the high resolution timer services
#include <mmsystem.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CTracker
IMPLEMENT_DYNCREATE(CTracker, CCmdTarget)
CTracker::CTracker()
{
EnableAutomation();
// make sure that the application won't unload until the reference count is
::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)
oTimeStamp.Format("%Y%m%d"));
// 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"));
}
}
CTracker::~CTracker()
{
// 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);
// make sure that the application can unloaded
::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 next step is to make the sample more meaningful by adding methods and properties, which 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 C++ and VB. Methods can pass parameters by value or by reference and may also 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, and when passing by reference, the address of the parameter is passed, allowing the method to change the data.

Optional parameters are handled a little differently than C++, however, since you can't specify a default value in the traditional C++ sense. Optional parameters must be passed as VARIANT data types and not the actual data type they represent.

For developers using VB to access a method with optional parameters, VB will supply the parameter for you if one has not been provided. With C++, you are still required to supply a VARIANT parameter even though it may not contain any 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, which is 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.

From the View menu, select the ClassWizard menu item. Select the Automation tab, and click the Add Method button. In the Add Method dialog enter an External name of OutputLines and a Return type of BOOL (see fig. 3.5).

FIG. 3.5
Add the OutputLines method with the ClassWizard.

Boolean Data Type Differences Between VC++ and VB
It is important to note a fundamental difference between VC++ and VB when using Boolean data types. The Boolean data type is defined by C++ as being of type integer that is a 32-bit value. For VB, however, an integer is 16-bit. For simple MFC-based Automation Servers, the difference in sizes between a VB integer and VC++ integer is not a problem since MFC hides the details involving the conversion of the 32-bit value to a 16-bit value, and vice versa. For dual-interface applications, though, the size difference poses a significant problem. When accessing the custom interface of a dual-interface server, the functions are called in the same fashion as any other function in an application. Basically, the parameters of the function are pushed on to a stack, and the function is called. When the function executes, the parameters are then popped off the stack. If VB calls a function in VC++, the stack will become corrupt because of the different sizes that each language uses for the Boolean data type. To be safe, VC++ applications should define all Boolean data types as type VARIANT_BOOL, which is defined by OLE as a 16-bit value and which is guaranteed to be the same size regardless of the language or tool being used. The actual Boolean data value is different between VB and VC++ also. VB developers are used to Boolean values of 0 indicating FALSE and -1 indicating TRUE. For those of you who may be wondering why, the binary values for each is 00000000 and 11111111, respectively. For C++ programmers, Boolean data values are usually defined as 0 for FALSE and 1 or non-zero for TRUE.

The differences in Boolean data values can cause considerable problems when integrating VB and VC++ applications. In addition, VB 4 has some behavioral differences in its language, depending on the value being tested. Some VB functions do not test for 0 or non-zero and will test for the absolute value of 0 or -1, and vice versa, depending on the data type and function. When using Boolean data types, it is wise to also use the VARIANT_FALSE and VARIANT_TRUE constants to define the value of the variable.

OutputLines is defined as having two parameters: varOutputArray as a VARIANT passed by reference, which will contain a string array of data to output to the file, and varIndent as a VARIANT passed by value, which is also an optional parameter indicating the amount of indentation when writing the string data to the file. To add the method parameters, double-click the line in the Parameter list that is directly below the Name column, and type varOutputArray. Click directly under the Type column to activate the Type drop-down list box. Select VARIANT * from the list. Repeat the same process for varIndent, but set the data type to VARIANT.

Due to data type restrictions imposed by Automation, you cannot pass arrays as parameters 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 (see Listing 3.6). varIndent is an optional parameter that indents the text output as an added formatting feature.

Click OK to add the method.

Click OK in the MFC ClassWizard dialog to close the ClassWizard. Remember that the ClassWizard also added an entry to the ODL file as well as the header and source files.

It is a function of the ODL file to declare a parameter of a method as being optional. To be optional, a parameter must be declared with the optional parameter attribute (see Listing 3.6), which you are required to add by hand since the ClassWizard will not add it for you.

Listing 3.6 MFCSERVER.ODL--Updated ODL Entry for OutputLines Method

// NOTE - ClassWizard will maintain method information here.
// Use extreme caution when editing this section.
//{{AFX_ODL_METHOD(CTracker)
[id(1)] boolean OutputLines(VARIANT* varOutputArray,
[optional] VARIANT varIndent);

//}}AFX_ODL_METHOD

Before you add the implementation of the OutputLines method, you need to add a member variable to the class definition (see Listing 3.7). The new member, m_lIndent, is used to store the current indentation level between calls to the method OutputLines.

Listing 3.7 TRACKER.H--New Member Variable Added to the Tracker Class

protected:
FILE * m_fileLog;
long m_lTimeBegin;
long m_lHiResTime;
long m_lLastHiResTime;
long m_lIndent;

};

You also need to update the constructor to initialize the member to a valid starting value (see Listing 3.8).

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

CTracker::CTracker()
{
. . .
m_lIndent = 0;
}

Listing 3.9 shows the implementation of the OutputLines method. First you check to see if you have a valid file handle and array of string data. The next step is to lock down the array so that you can perform operations on it. This step is required for all functions that manipulate safe arrays. The next function determines the starting point of the array, which can be either 0 or 1. This procedure is very important to implement since programming languages such as C++ define a base of 0 for arrays, and languages such as VB can define a base of 0 or 1. Next you retrieve the number of dimensions in the array. Note that this value represents the number of dimensions and not the last dimension relative to the lower bound value.

After establishing the boundaries of the array, you check to see if you have received an indentation value also. You want to receive a long, VT_I4, but if you don't receive it, you try to convert the data that was given to you to a usable value. If you can't convert the data, you simply use the value that the variable already contains. To indent the text, concatenate from 1 to n tab characters into a string.

For each of the elements in the array of strings, get the current time and the data associated with each element, and output them along with the indentation string to the open file--and don't forget to free the string element when you finish with it.

The last step is to unlock the array and exit the method with the proper return value.

Listing 3.9 TRACKER.CPP--OutputLines Method Implementation

. . .
/////////////////////////////////////////////////////////////////////////////
// CTracker message handlers
BOOL CTracker::OutputLines(VARIANT FAR* varOutputArray, const VARIANT FAR& varIndent)
{
BOOL bResult = 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
bResult = VARIANT_FALSE;
}
else
bResult = VARIANT_FALSE;
// unlock the array we don't need it anymore
::SafeArrayUnlock(varOutputArray->parray);
}
else
bResult = VARIANT_FALSE;
}
else
bResult = VARIANT_FALSE;
// return the result
return bResult;

}

It is worthwhile to review the documentation in the VC++ books online on ODL, Automation, and VARIANT data types to see what kind of flexibility you have when defining methods and parameters. See Chapters 6 through 11 on developing ActiveX controls for descriptions of more options and features when creating methods.

Now that you have added a method, you are ready 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.

Properties are implemented as a pair of methods: one to get the value, and the other to set the value. The m_lIndent member variable that you added to the class definition is a perfect candidate to be exposed as a property.

As with methods, properties are also added via the ClassWizard in MFC. From the View menu, select the ClassWizard menu item. In the MFC ClassWizard dialog, select the Automation tab, and click the Add Property button. In the Add Property dialog, enter the External name of the property as Indent and select the type as long (see fig. 3.6). Set the Implementation to Get/Set methods, and click OK to add the property to the server. Click the Edit Code button to close the MFC ClassWizard dialog and open the source file for editing.

FIG. 3.6
Add the Indent property with the ClassWizard.

The actual implementation of the Indent property is very easy (see Listing 3.10). GetIndent returns the value currently stored in the member variable, and SetIndent stores the new value, after a little bit of error checking, in the member variable.

Listing 3.10 TRACKER.CPP--Indent Property Implementation

long CTracker::GetIndent()
{
// return the member variable
return m_lIndent;
}
void CTracker::SetIndent(long nNewValue)
{
// if the new value is a least 0
if(nNewValue >= 0)
// assign the value to our member variable
m_lIndent = nNewValue;

}

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 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 it is 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. It is necessary to 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 on 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 create 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 internal to an application's implementation, and OLE exceptions are used externally to communicate errors to other applications.

The IDispatch interface contains specific parameters in its functions for dealing with exceptions and passing them to the controller application. The MFC implementation of the CCmdTarget class handles the details of generating OLE exceptions by trapping any C++ exceptions that it receives and translates them to the proper IDispatch error information. You need only create a C++ exception of type COleDispatchException and throw it. MFC does all of the work for you. When creating dual-interface servers, exceptions are handled in a different way, which you will see later in this chapter.

The first step is to add an enumeration of the types of errors that the server can generate to the ODL file (see Listing 3.11). Adding the enumeration to the ODL has the effect of publishing the error constants to the applications developer that is using the server. You add the constants in the form of an include file so that you can use the same error constants file in the C++ source code implementation. You also add a UUID to the typedef so that it can be identified in the type library that is generated with the GUIDGEN.EXE program.

Listing 3.11 MFCSERVER.ODL--Error Enumeration

. . .
[ uuid(11C82947-4EDD-11D0-BED8-00400538977D) ]
coclass TRACKER
{
[default] dispinterface ITracker;
};
typedef [uuid(11C82948-4EDD-11D0-BED8-00400538977D), helpstring("Tracker Error Constants")]
#include "trackererror.h"
//{{AFX_APPEND_ODL}}

};

TrackerError.h contains a standard C/C++ enumeration of the errors that the application supports (see Listing 3.12). The starting value of the errors falls into the range of valid user-defined errors. Be careful when assigning error numbers since most tools will first look up the system-defined error message before using the message defined in the exception.

Listing 3.12 TRACKERERROR.H--Tracker Error Constants

// Error enumeration
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;

The next step is to add the code that will generate the exceptions to all of the appropriate locations in the server code (see Listing 3.13). As you can see, instead of returning VARIANT_FALSE or ignoring an error condition, you now generate meaningful errors and messages instructing the developer as to the source of the problem. The exception-generating code is fairly straightforward. First you create a COleDispacthException and set the appropriate members with the data that is necessary for the error that was generated (for information about other types of exceptions, see the VC++ documentation). For your implementation, you set the error code, the name of the file that generated the error, and the error message. You could also supply a help filename and a help ID to further describe the error. Note the use of the MAKE_SCODE macro to generate a valid SCODE error number for the exception.

Listing 3.13 TRACKER.CPP--Exception Handling Code Added to the Source Files

/////////////////////////////////////////////////////////////////////////////
// CTracker message handlers
BOOL CTracker::OutputLines(VARIANT FAR* varOutputArray, const VARIANT FAR& varIndent)
{
BOOL bResult = 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
{
bResult = VARIANT_FALSE;
// unable to get a record based on the sql statement - throw an exception
COleDispatchException * pOleDispExcep = new COleDispatchException(_T(""), NULL, 0);
// format the error code
pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_UBOUND);
// set the source file
pOleDispExcep->m_strSource = __FILE__;
// format the error description
pOleDispExcep->m_strDescription = _T("Unable to retrieve the upper bound dimension of the array.");
// the function call failed cause an ole exception
throw(pOleDispExcep);
}
}
else
{
bResult = VARIANT_FALSE;
// unable to get a record based on the sql statement - throw an exception
COleDispatchException * pOleDispExcep = new COleDispatchException(_T(""), NULL, 0);
// format the error code
pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_LBOUND);
// set the source file
pOleDispExcep->m_strSource = __FILE__;
// format the error description
pOleDispExcep->m_strDescription = _T("Unable to retrieve the lower bound dimension of the array.");
// the function call failed cause an ole exception
throw(pOleDispExcep);
}
// unlock the array we don't need it anymore
::SafeArrayUnlock(varOutputArray->parray);
}
else
{
bResult = VARIANT_FALSE;
// unable to get a record based on the sql statement - throw an exception
COleDispatchException * pOleDispExcep = new COleDispatchException(_T(""), NULL, 0);
// format the error code
pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_ARRAYLOCK);
// set the source file
pOleDispExcep->m_strSource = __FILE__;
// format the error description
pOleDispExcep->m_strDescription = _T("Unable to lock the array memory.");
// the function call failed cause an ole exception
throw(pOleDispExcep);
}
}
else
{
bResult = VARIANT_FALSE;
// unable to get a record based on the sql statement - throw an exception
COleDispatchException * pOleDispExcep = new COleDispatchException(_T(""), NULL, 0);
// if we didn't have a file handle
if(!m_fileLog)
// format the error code
pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_FILE);
else
// format the error code
pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_BAD_ARRAY_PARAMETER);
// set the source file
pOleDispExcep->m_strSource = __FILE__;
// if we didn't have a file handle
if(!m_fileLog)
// format the error description
pOleDispExcep->m_strDescription = _T("Invalid File Handle. File could not be opened for output.");
else
// format the error description
pOleDispExcep->m_strDescription = _T("The first parameter must be a string array passed by reference.");
// the function call failed cause an ole exception
throw(pOleDispExcep);
}
// return the result
return bResult;
}
long CTracker::GetIndent()
{
// return the member variable
return m_lIndent;
}
void CTracker::SetIndent(long nNewValue)
{
// if the new value is a least 0
if(nNewValue >= 0)
// assign the value to our member variable
m_lIndent = nNewValue;
else
{
// unable to get a record based on the sql statement - throw an exception
COleDispatchException * pOleDispExcep = new



COleDispatchException(_T(""), NULL, 0);
// format the error code
pOleDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_INVALID_VALUE);
// set the source file
pOleDispExcep->m_strSource = __FILE__;
// format the error description
pOleDispExcep->m_strDescription = _T("Invalid value. Value must be 0 or greater.");
// the function call failed cause an ole exception
throw(pOleDispExcep);
}

}

Exceptions are useful for communicating error conditions and problems back to the application and developer who are using an ActiveX component. Make use of them whenever you can to further enhance your implementation.

Dual-Interface

Dual-interface is exactly what it sounds like: The server implementation supports two interfaces with which to talk to the server. One interface, an IDispatch interface, is what you have been working with so far. The other, a custom interface, is a type of interface that you have not looked at yet. The dual portion refers to the fact that no matter which interface you choose, you are always talking to the same server, and you will always get the same response.

An IDispatch-based interface uses a generic mechanism for calling methods and properties in a server. When a method in a server is called, you pass the ID of the method to invoke and a structure describing its parameters and return type. This data is packaged and sent to the server, which unpackages the data and calls the appropriate method based on the ID supplied.

A custom interface, on the other hand, is very different. When using a custom interface, you are talking directly to the server's functions and are not depending on a generic mechanism for invoking the methods or properties. The packaging of the parameters and return value are left to the compiler that created the applications.

Since the interfaces are written to access the same set of functions, the custom interface portion of a dual-interface server must conform to the same data type restrictions imposed by Automation. This way, you are not required to create your own code to transfer data between the two applications: the controller and the server. OLE does that for you with standard marshaling.

The major advantage to dual-interface is performance. The number of steps to call a method using the custom interface is far less than the number needed to call a method when using the IDispatch interface.

The main disadvantage to dual-interface support in MFC servers is that they are not supported by the ClassWizard and will require manual changes to implement. The code involved is not too difficult to maintain; the hardest part is probably remembering to do it.

NOTE: The advantage of using the custom interface of a dual-interface server loses its luster when executing across process boundaries. The custom interface is still faster, but not by much. The real performance benefit, a 25 to 50 percent improvement, is when the server is in-process to the calling application. The amount of improvement depends on the number and types of parameters that are being passed between the applications. If you are interested in seeing actual numbers regarding the amount of performance improvement, you can refer to recent Microsoft Systems Journal articles, which have focused on performance differences between IDispatch and custom interfaces.



The first step when converting an MFC-based ActiveX server to dual-interface is to change the ODL file. Listing 3.14 shows the changes that have been made to the server to support dual-interface. It is not necessary to generate new UUIDs because the functionality of the server has not changed. You must add the oleautomation and dual attributes to the interface class, though. You also add the hidden attribute, so this interface will not be visible within VB. This is fine since VB will display the CoClass interface and will also show all of the methods and properties for the server. As a general rule, you should always hide your interfaces and leave visible the CoClasses used to create them. This is because applications like VB will display both interfaces, and the reality is that only the CoClass name is valid in VB; if you try to reference an object by its interface name, you will get an error.

Since the server supports dual-interface, you must change your interface declaration to inherit from the IDispatch interface rather than declare the interface as type dispinterface, as in the original implementation. Dual-interface method and property declarations are different from dispinterface declarations, which are more like standard C++. Note that keywords such as method and properties are no longer within the interface declaration. Those terms are keywords related to the dispinterface keyword. As we stated earlier, properties are accessed using a pair of methods sharing the same dispid. The distinguishing factors are the method attributes propget and propput, which denote the direction of data flow. You must also change the CoClass to refer to interface and not dispinterface.

All dual-interface methods must return an HRESULT data type. If a method requires a return value, it must be specified as the last parameter of the method and must have the parameter attributes of out and retval. All parameters must have an attribute describing the direction of data flow. See Table 3.3 for a complete description of the possible attributes and combinations.
Table 3.3 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.

Listing 3.14 MFCSERVER.ODL--ODL Changes to Support Dual-Interface

[ uuid(11C82943-4EDD-11D0-BED8-00400538977D), version(1.0) ]
library MFCServer
{
importlib("stdole32.tlb");

[ uuid(11C82946-4EDD-11D0-BED8-00400538977D), oleautomation, dual, hidden ]
interface ITracker: IDispatch
{
[id(1), propget] HRESULT Indent([out, retval] long * Value);
[id(1), propput] HRESULT Indent([in] long Value);
[id(2)] HRESULT OutputLines([in] VARIANT * varOutputArray, [in, optional] VARIANT varIndent, [out, retval] boolean * RetVal);
};
// CoClass for CTracker
[ uuid(11C82947-4EDD-11D0-BED8-00400538977D) ]
coclass TRACKER
{
[default] interface ITracker;
};
typedef [uuid(11C82948-4EDD-11D0-BED8-00400538977D), helpstring("Tracker Error Constants")]
#include "trackererror.h"
//{{AFX_APPEND_ODL}}

};

The remainder of the ODL file entries do not have to be changed to support dual-interface.

NOTE: The changes that have been made to the ODL file will prevent the ClassWizard from updating the ODL file automatically when new methods and properties are added. You are now responsible for maintaining the entries manually.



The ODL compiler has the capability of generating a C++ header file that describes all of the interfaces and enumerations that are contained in the type library that you've created for your server. The ODL-generated header file is useful for creating function prototypes that are required in the implementation of the server. You add the entry to the ODL file and compile it into a type library. Next copy the new method from the header file into your class definition and make some minor changes--and everything is finished. In addition, you now have an interface file that can be used by other applications to access the custom interface of your server as well as the enumerations that are used when accessing specific methods and properties.

To generate the header file, you must update the build settings of your project. From the Project menu, select the Settings menu item. In the Settings For drop-down list box found in the Project Settings dialog, select All Configurations (see fig. 3.7). Expand the MFCServer project node and the Source Files node, and select the MFCServer.odl file. Select the OLE Types tab, and in the Output header file name edit field, enter the name TrackerInterface.h. Whenever the type library is compiled, the TrackerInterface.h will be regenerated to reflect the new implementation.

FIG. 3.7
Update the project settings to create the C++ header file from the ODL file.

Listing 3.15 shows the inclusion of the new header file to the Tracker.cpp source file. Note that you also remove the TrackerError.h file because the error enumeration that you declared earlier in the chapter is also defined in the header file generated by the ODL compiler. The TrackerInterface.h must be included before or in the Tracker.h file; the CTracker class is dependent on the information in the TrackerInterface.h file.

Listing 3.15 TRACKER.CPP--ODL-Generated Header File Is Added to the Tracker Source File

. . .
#include "MFCServer.h"
// ODL generated interface file
#include "trackerinterface.h"
#include "Tracker.h"
// needed for the high resolution timer services
#include <mmsystem.h>
#ifdef _DEBUG
#define new DEBUG_NEW

. . .

MFC defines a set of macros for describing interfaces within the context of an MFC component implementation. The interface macro defines the interface, its name, and the methods that it contains. Listing 3.16 shows the MFC interface declaration that is added to your class definition to describe the custom interface portion of your server. The interface declaration can be added anywhere after the DECLARE_INTERFACE_MAP macro.

The first parameter of the BEGIN_INTERFACE_PART macro, SubDispatch, is the name that is used to create a nested class within the class definition of the server. The second parameter is the name of the interface from which the nested class is inherited. The ITracker interface is declared in the TrackerInterface.h header file that was created from the ODL file. The ITracker declaration in the header file contains a set of pure virtual functions that need to be copied into your interface declaration. When copying the functions, remember to remove the = 0 from the end of the function, since this is where you will implement them.

Listing 3.16 TRACKER.H--Interface Macro Update of the Tracker Class Definition

. . .
// needed for dual interface support
BEGIN_INTERFACE_PART(SubDispatch, ITracker)
STDMETHOD(GetTypeInfoCount)(THIS_ UINT FAR* pctinfo);
STDMETHOD(GetTypeInfo)(THIS_ UINT itinfo, LCID lcid,
ITypeInfo FAR* FAR* pptinfo);
STDMETHOD(GetIDsOfNames)(THIS_ REFIID riid,
OLECHAR FAR* FAR* rgszNames, UINT cNames, LCID lcid,
DISPID FAR* rgdispid);
STDMETHOD(Invoke)(THIS_ DISPID dispidMember, REFIID riid,
LCID lcid, WORD wFlags, DISPPARAMS FAR* pdispparams,
VARIANT FAR* pvarResult, EXCEPINFO FAR* pexcepinfo,
UINT FAR* puArgErr);
virtual /* [propget][id] */ HRESULT STDMETHODCALLTYPE get_Indent(
/* [retval][out] */ long __RPC_FAR *Value);

virtual /* [propput][id] */ HRESULT STDMETHODCALLTYPE put_Indent(
/* [in] */ long Value);

virtual /* [id] */ HRESULT STDMETHODCALLTYPE OutputLines(
/* [in] */ VARIANT __RPC_FAR *varOutputArray,
/* [optional][in] */ VARIANT varIndent,
/* [retval][out] */ VARIANT_BOOL __RPC_FAR *RetVal);


END_INTERFACE_PART(SubDispatch). . .

The header file contains the interface declaration, and the source file contains the interface implementation. The MFC AppWizard created a default interface implementation for the server when it was created. The original definition deferred to the default IDispatch interface defined in MFC. Since you have implemented an interface that inherits from IDispatch, it is necessary to route all IDispatch calls to the new interface. The interface map located in the server source file must be changed to reflect the new interface that you've declared. Change the interface from IDispatch to the name of the interface declared in the header file, in your case SubDispatch, as in Listing 3.17. Changing the name of the interface will have the effect of routing all calls to the IDispatch interface to your implementation of the interface first, which you can then implement yourself or pass on to the default implementation.

Unfortunately, MFC does not allow for true C++ inheritance of the COM interfaces it contains, so it is necessary to route messages for a particular COM interface to the correct handler function in the server. Routing the messages is done through the BEGIN_INTERFACE_MAP macro. Since your implementation supports both an IDispatch interface and a custom interface, you are required to add to entries to the macro. In your case, you defer all IDispatch messages to the custom interface functions. The same is true of the custom interface messages.

Listing 3.17 TRACKER.CPP--Interface Implementation of the ITracker Interface

. . .
BEGIN_INTERFACE_MAP(CTracker, CCmdTarget)
INTERFACE_PART(CTracker, IID_IDispatch, SubDispatch)
INTERFACE_PART(CTracker, IID_ITracker, SubDispatch)
END_INTERFACE_MAP()

. . .

The last step in adding dual-interface support to your server is to add the actual implementation of the functions declared in the interface. The first set of functions to implement is the base IDispatch functions that your server inherited from the IDispatch class. Listing 3.18 shows the implementation of the functions. In all cases, you defer to the base class implementation of the method. You have the option to implement these functions yourself or rely on MFC to handle the methods for you. An important thing to note is the method used to call the basic IDispatch functions. Since the server captures all IDispatch messages and routes them to the custom interface implementation, you cannot call GetIDispatch() to retrieve the pointer to the IDispatch interface of the server. The reason for this is that it will result in a recursive call since the IDispatch functions are routed to your server implementation; instead, the implementation calls the IDispatch functions ((IDispatch*)&pThis->m_xDispatch)-> directly, bypassing the message routing functions, thus avoiding the recursion problem. You also note that the nested class SubDispatch is declared as XSubDispatch in the implementation. The X comes from the BEGIN_INTERFACE_PART macros and has no particular significance. The same is true for the member variable m_xDispatch.

Note the use of the macro METHOD_PROLOGUE, which is required in MFC-based applications to ensure that MFC is in a valid state while the function is executing.

Listing 3.18 TRACKER.CPP--IDispatch Function Implementation for a Dual-Interface Server

/////////////////////////////////////////////////////////////////////////////
// CTracker Standard IDispatch Dual Interface Handlers
ULONG FAR EXPORT CTracker::XSubDispatch::AddRef()
{
METHOD_PROLOGUE(CTracker, SubDispatch)
return pThis->ExternalAddRef();
}
ULONG FAR EXPORT CTracker::XSubDispatch::Release()
{
METHOD_PROLOGUE(CTracker, SubDispatch)
return pThis->ExternalRelease();
}
HRESULT FAR EXPORT CTracker::XSubDispatch::QueryInterface(REFIID riid, LPVOID FAR* ppvObj)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
return (HRESULT) pThis->ExternalQueryInterface(&riid, ppvObj);
}
HRESULT FAR EXPORT CTracker::XSubDispatch::GetTypeInfoCount(UINT FAR* pctinfo)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
return ((IDispatch*)&pThis->m_xDispatch)->GetTypeInfoCount(pctinfo);
}
HRESULT FAR EXPORT CTracker::XSubDispatch::GetTypeInfo(UINT itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
return ((IDispatch*)&pThis->m_xDispatch)->GetTypeInfo(itinfo, lcid, pptinfo);
}
HRESULT FAR EXPORT CTracker::XSubDispatch::GetIDsOfNames(REFIID riid,
OLECHAR FAR* FAR* rgszNames, UINT cNames, LCID lcid, DISPID FAR* rgdispid)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
return ((IDispatch*)&pThis->m_xDispatch)->GetIDsOfNames(riid, rgszNames, cNames, lcid, rgdispid);
}
HRESULT FAR EXPORT CTracker::XSubDispatch::Invoke(DISPID dispidMember, REFIID riid,
LCID lcid, WORD wFlags, DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult,
EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
return ((IDispatch*)&pThis->m_xDispatch)->Invoke(dispidMember, riid, lcid, wFlags, pdispparams, pvarResult, pexcepinfo, puArgErr);

}

The last step is to implement the functions that are specific to your server. As can be seen in Listing 3.19, you simplified the implementation by calling the original MFC functions from the dual-interface implementations rather than reimplementing the functions using the new style. Doing so solves several problems. First, you can still rely on the MFC message mapping functions to invoke the methods when they are called via the IDispatch interface, and it does not require you to implement your own IDispatch code. Supporting the original MFC implementation also prevents you from having to change internal code that may rely on the already existing functions. As you will see a little later in this chapter, adding error handling to the dual-interface code is much simpler using this style.

Listing 3.19 TRACKER.CPP--ITracker Function Implementation

/////////////////////////////////////////////////////////////////////////////
// CTracker interface handlers
HRESULT CTracker::XSubDispatch::get_Indent(LONG * Indent)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
HRESULT hResult = S_OK;
*Indent = pThis->GetIndent();
return hResult;
}
HRESULT CTracker::XSubDispatch::put_Indent(LONG Indent)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
HRESULT hResult = S_OK;
pThis->SetIndent(Indent);
return hResult;
}
HRESULT FAR EXPORT CTracker::XSubDispatch::OutputLines(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
HRESULT hResult = S_OK;
*RetVal = pThis->OutputLines(varOutputArray, varIndent);
return hResult;

}

After compiling the server and registering it, you are ready to test its new dual-interface functionality. From C++, you will create the server as you did before, but now you may access its custom interface by using the QueryInterface function supplying the correct interface IID. From VB, you need only add the type library for the server to your list of references and change the name of the variable to the name that appears in the Object References dialog. You can also create the server using New instead of the CreateObject method.

In VB, you call

Dim MyTracker as TRACKER
Set MyTracker = new TRACKER

instead of

Dim MyTracker as Object
Set MyTracker = CreateObject("MFCServer.Tracker")

In C++, you call QueryInterface passing the IID ITracker interface IID. The last step in your dual-interface conversion is to handle errors correctly. As a general rule, a server cannot throw C++ exceptions from the custom interface implementation of a dual-interface server. For that matter, it can't throw them from any interface.

MFC just does the job of catching the C++ exceptions for you and converting them to OLE exceptions that are understood by OLE. Since you are supporting a custom interface in your server, you must do the same.

Generating Dual-Interface OLE Exceptions

Dual-interface rules for handling errors and exceptions is slightly different for the custom interface portion of the interface. As you saw earlier, all of the methods were changed to return an HRESULT in place of the function's normal return value. An HRESULT is used to indicate that an error or exception has occurred within the context of the method that was invoked. When an automation controller invokes a method in the custom interface of a server, it should check the return value of the function to see if it returned S_OK. If not, the controller has the option of checking to see whether the server supports extended error information via the ISupportErrorInfo interface. When a server that supports the ISupportErrorInfo interface creates an error, it does so by creating an IErrorInfo object containing the error information.

MFC does not support the ISupportErrorInfo interface by default, so you must add it yourself. We've created a set of macros to aid in adding the ISupportErrorInfo interface to your Automation Server (see Listing 3.20). We've used the macros defined in the ACDUAL sample included with MFC to simplify the ISupportErrorInfo implementation.

Listing 3.20 ERRORINFOMACROS.H--ISupportErrorInfo Helper Macros

// this code is based on the ACDUAL MFC\OLE sample application provided with Visual C++
/////////////////////////////////////////////////////////////////////
// DECLARE_DUAL_ERRORINFO expands to declare the ISupportErrorInfo
// support class. It works together with DUAL_ERRORINFO_PART and
// IMPLEMENT_DUAL_ERRORINFO defined below.
#define DECLARE_DUAL_ERRORINFO() \
BEGIN_INTERFACE_PART(SupportErrorInfo, ISupportErrorInfo) \
STDMETHOD(InterfaceSupportsErrorInfo)(THIS_ REFIID riid); \
END_INTERFACE_PART(SupportErrorInfo) \
/////////////////////////////////////////////////////////////////////
// DUAL_ERRORINFO_PART adds the appropriate entry to the interface map
// for ISupportErrorInfo, if you used DECLARE_DUAL_ERRORINFO.
#define DUAL_ERRORINFO_PART(objectClass) \
INTERFACE_PART(objectClass, IID_ISupportErrorInfo, SupportErrorInfo) \
/////////////////////////////////////////////////////////////////////
// IMPLEMENT_DUAL_ERRORINFO expands to an implementation of
// ISupportErrorInfo which matches the declaration in
// DECLARE_DUAL_ERRORINFO.
#define IMPLEMENT_DUAL_ERRORINFO(objectClass, riidSource) \
STDMETHODIMP_(ULONG) objectClass::XSupportErrorInfo::AddRef() \
{ \
METHOD_PROLOGUE(objectClass, SupportErrorInfo) \
return pThis->ExternalAddRef(); \
} \
STDMETHODIMP_(ULONG) objectClass::XSupportErrorInfo::Release() \
{ \
METHOD_PROLOGUE(objectClass, SupportErrorInfo) \
return pThis->ExternalRelease(); \
} \
STDMETHODIMP objectClass::XSupportErrorInfo::QueryInterface( \
REFIID iid, LPVOID* ppvObj) \
{ \
METHOD_PROLOGUE(objectClass, SupportErrorInfo) \
return pThis->ExternalQueryInterface(&iid, ppvObj); \
} \
STDMETHODIMP objectClass::XSupportErrorInfo::InterfaceSupportsErrorInfo( \
REFIID iid) \
{ \
METHOD_PROLOGUE(objectClass, SupportErrorInfo) \
return (iid == riidSource) ? S_OK : S_FALSE; \

}

Include the ErrorInfoMacros.h file in the server source file, as in Listing 3.21.

Listing 3.21 TRACKER.CPP--ISupportErrorInfo Include File

. . .
#include "stdafx.h"
#include "MFCServer.h"
// error info support
#include "ErrorInfoMacros.h"
// ODL generated interface file
#include "trackerinterface.h"

. . .

You need to add a macro and a helper function to the class declaration of the server (see Listing 3.22). The macro declares the ISupportErrorInfo interface, and the helper function is used to translate the exception into an IErrorInfo object.

Listing 3.22 TRACKER.H--ISupportErrorInfo Class Declaration

. . .
DECLARE_OLECREATE(CTracker)
// add declaration of ISupportErrorInfo implementation
// to indicate we support the OLE Automation error object
DECLARE_DUAL_ERRORINFO()
HRESULT CreateErrorInfo(CException * pAnyException, REFIID riidSource);
// needed for dual interface support
BEGIN_INTERFACE_PART(SubDispatch, ITracker)

. . .

Now that you have your interface declaration, you need to add it to the interface map. You also need to add the implementation macro for the interface and add the implementation of the helper function (see Listing 3.23).

The CreateErrorInfo function translates any exception into an IErrorInfo object. The function is based on code that is part of the ACDUAL MFC sample application included with MFC. The primary responsibility of the function is to convert COleDispatchExceptions into IErrorInfo objects. It can, however, deal with any exception that it is passed, but the level of information about the error is far less. After the exception has been translated, it is set as the error information object for the currently executing thread.

Listing 3.23 TRACKER.CPP--ISupportErrorInfo Interface Implementation

BEGIN_INTERFACE_MAP(CTracker, CCmdTarget)
INTERFACE_PART(CTracker, IID_ITracker, SubDispatch)
DUAL_ERRORINFO_PART(CTracker)
END_INTERFACE_MAP()
IMPLEMENT_OLECREATE(CTracker, _T("MFCServer.Tracker"), 0x11C82947, 0x4edd, 0x11d0, 0xbe, 0xd8, 0x0, 0x40, 0x5, 0x38, 0x97, 0x7d)
// Implement ISupportErrorInfo to indicate we support the
// OLE Automation error handler.
IMPLEMENT_DUAL_ERRORINFO(CTracker, IID_ITracker)
// this code is based on the ACDUAL MFC\OLE sample application provided with Visual C++
HRESULT CTracker::CreateErrorInfo(CException * pAnyException, REFIID riidSource)
{
ASSERT_VALID(pAnyException);
// create an error info object
ICreateErrorInfo * pcerrinfo;
HRESULT hr = ::CreateErrorInfo(&pcerrinfo);
// if we succeeded
if(SUCCEEDED(hr))
{
// dispatch exception?
if(pAnyException->IsKindOf(RUNTIME_CLASS(COleDispatchException)))
{
// specific IDispatch style exception
COleDispatchException * e = (COleDispatchException *) pAnyException;
// set the return value to the error
hr = e->m_scError;
// Set up ErrInfo object
pcerrinfo->SetGUID(riidSource);
pcerrinfo->SetDescription(e->m_strDescription.AllocSysString());
pcerrinfo->SetHelpContext(e->m_dwHelpContext);
pcerrinfo->SetHelpFile(e->m_strHelpFile.AllocSysString());
pcerrinfo->SetSource(e->m_strSource.AllocSysString());
}
else if (pAnyException->IsKindOf(RUNTIME_CLASS(CMemoryException)))
{
// failed memory allocation
hr = E_OUTOFMEMORY;
// Set up ErrInfo object
pcerrinfo->SetGUID(riidSource);
CString cstrFileName(AfxGetAppName());
pcerrinfo->SetSource(cstrFileName.AllocSysString());
}
else
{
// other unknown/uncommon error
hr = E_UNEXPECTED;
// Set up ErrInfo object
pcerrinfo->SetGUID(riidSource);
CString cstrFileName(AfxGetAppName());
pcerrinfo->SetSource(cstrFileName.AllocSysString());
}
// QI for the IErrorInfo interface
IErrorInfo * perrinfo;
if(SUCCEEDED(pcerrinfo->QueryInterface(IID_IErrorInfo, (LPVOID *) &perrinfo)))
{
// set the error info object
::SetErrorInfo(0, perrinfo);
// release the reference
perrinfo->Release();
}
// release the reference
pcerrinfo->Release();
}
// delete the exception
pAnyException->Delete();
// return the error value
return hr;

}

The last step is to update the custom interface methods to defer all exceptions to the helper function that you just added. The additional code needed is straightforward and simple to implement (see Listing 3.24). For each of the methods, you wrap the call to the basic implementation of the function with a try...catch block that will translate any exception into an IErrorInfo object and return the error code of the exception to the calling application.

Listing 3.24 TRACKER.CPP--Custom Interface Exception Handling Code

/////////////////////////////////////////////////////////////////////////////
// CTracker interface handlers
HRESULT CTracker::XSubDispatch::get_Indent(LONG * Indent)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
HRESULT hResult = S_OK;
try
{
*Indent = pThis->GetIndent();
}
catch(CException * pException)
{
hResult = pThis->CreateErrorInfo(pException, IID_ITracker);
}
return hResult;
}
HRESULT CTracker::XSubDispatch::put_Indent(LONG Indent)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
HRESULT hResult = S_OK;
try
{
pThis->SetIndent(Indent);
}
catch(CException * pException)
{
hResult = pThis->CreateErrorInfo(pException, IID_ITracker);
}
return hResult;
}
HRESULT FAR EXPORT CTracker::XSubDispatch::OutputLines(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
HRESULT hResult = S_OK;
try
{
*RetVal = pThis->OutputLines(varOutputArray, varIndent);
}
catch(CException * pException)
{
hResult = pThis->CreateErrorInfo(pException, IID_ITracker);
}
return hResult;

}

You've covered all of the basics of ActiveX server creation and use by applications other than your own. What if you need to create and use the server from within the application in which it is defined? Or perhaps your application contains more than one server implementation, with only one server being exposed as a creatable object and with the remaining servers being created only in response to a valid method call in the exposed server. These are referred to as nested objects.

Server Instantiation Using C++

OLE is not the only method for creating and using Automation Servers. This chapter will show you how to instantiate Automation Servers using C++ syntax.

At times, instantiating and using Automation Servers is necessary from within the application in which they are defined. Take, for example, a case where an application contains three servers, only one of which is 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.

As we stated earlier in this chapter, in order for an application to be created by another application using OLE, the server must include the MFC macros DECLARE_OLECREATE and IMPLEMENT_OLECREATE. By removing or not including these macros, an application cannot be instantiated using standard OLE server creation mechanisms. That fact, however, does not prevent the server from being created using C++ and MFC. Note, though, that any OLE server can be created in this fashion, as opposed to just those that are not creatable through standard OLE mechanisms.

MFC supports a facility for creating OLE servers using a helper class known as CRuntimeClass.

The CRuntimeClass can be used to create servers that will be used internally to the application in which they are defined and externally as a return or parameter value supplied to another application. To support CRuntimeClass creation of objects, a class must define either the IMPLEMENT_DYNAMIC, IMPLEMENT_DYNCREATE, or the IMPLEMENT_SERIAL macros within their class implementation, which is true for any MFC class inherited for Cobject, and not just those classes that utilize OLE.

The following listing is not included in the sample applications because of its simplicity. We have, however, used the CTracker server as our example server. To create a server using the CRuntimeClass, perform the following steps:


// create a CTracker runtime object
CRuntimeClass * pRuntimeClass = RUNTIME_CLASS(CTracker);
// create an CTracker OLE object
CTracker * opTracker = (CTracker *) pRuntimeClass->CreateObject();
. . . // use the object in anyway that is appropriate for the application
// finished with the object - destroy it

delete opTracker;

After an object is created, it can then also be passed to another application. MFC supports two functions for retrieving OLE interfaces from a running server: GetIDispatch and GetInterface. GetIDispatch is defined in the VC++ help file, but the GetInterface function is not. GetInterface accepts a single parameter of an IID of the interface that is being requested. GetInterface will not increment the reference count of the pointer that is returned. The GetIDispatch function gives you the option.

Problems Associated with Instantiating OLE Servers Using C++
Two problems arise when instantiating OLE components using C++: reference counting and in-process versus out-of-process execution. For reference counting, the problems can be that too many or too few reference counts exist on the server being used. Reference counting problems can arise regardless of how the server is instantiated, with either C++ or OLE. The problem is really relative to using servers in one to many relationships. All of the applications that use the same server must AddRef and Release the server properly to prevent problems. Reference counting problems manifest themselves as either the server terminating before it should or not terminating when it should. Be sure to check that all reference counts are being incremented and decremented correctly when creating and using objects in a one-to-many relationship or when a server is created and passed to another application. The next problem that can arise is far more subtle and easier to miss, although the effects can be dramatic and obvious and involve the server's execution model. A server is not guaranteed to execute in-process to the application that is using it--only with the application that created it.

Application A creates and uses an in-process server called Server 1. At some point, Application A creates an out-of-process server called Application B. Application A passes Server 1 to Application B. Server 1 will execute as an out-of-process server to Application B since the server was created in the process space of Application A. This is important to keep in mind when creating and using nested objects or shared objects since the performance differences between the two is so great.

So far you've only looked at how to create individual instances of objects. Next you will look at 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. Using shared objects is useful for applications that may need to work with a single running instance of an application rather than 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.

The first step in enabling shared object support is to add a member variable to store the ID that will identify the object in the Running Object Table. This ID must be retained since it is used later in revoking the object from the Running Object Table when the object is destroyed. For the sample implementation, you add the variable as a public member of the class (see Listing 3.25).

Listing 3.25 TRACKER.H--Shared Object Member Variable Added to the CTracker Class

. . .
END_INTERFACE_PART(SubDispatch)
public:
DWORD m_dwRegister;
protected:
FILE * m_fileLog;

. . .

When registering a server in the Running Object Table, you use the CLSID of the CoClass object to identify the object in the table. Your implementation requires that you declare the CLSID of the CoClass object in the source file. The CLSID is copied from the TrackerInterface.h header file and added to the source file. The line before the CLSID should be the include file initguid.h; this file is needed to properly declare the CLSID and resolve it to the compiler.

Listing 3.26 TRACKER.CPP--CLSID Declaration

. . .
static const IID IID_ITracker =
{ 0x11c82946, 0x4edd, 0x11d0, { 0xbe, 0xd8, 0x0, 0x40, 0x5, 0x38, 0x97, 0x7d }
#include <initguid.h>
DEFINE_GUID(CLSID_TRACKER,0x11C82947L,0x4EDD,0x11D0,0xBE,0xD8,0x00,0x40,0x05, 0x38,0x97,0x7D);
BEGIN_INTERFACE_MAP(CTracker, CCmdTarget)

. . .

Next you add the code to the server that registers the object as running in the Running Object Table. Listing 3.27 shows the implementation of the CTracker shared object support in the constructor of the CTracker class. The specifics of your server will determine the exact location where you register it as running. For your implementation, the constructor is fine. Other implementations may be dependent on a particular state being reached in the server before registering the server. The decision is completely up to you and your specific implementation.

In your implementation, the first step is to clear the member variable. The implementation is dependent on this member to identify whether the object was successfully registered as running or not. Next you retrieve the IUnknown reference for the object and, if successful, pass it along with an address of the member variable to the RegisterActiveObject function. You specify a strong registration that will result in an extra reference count on the server to keep it in memory. If you didn't register the server, you make sure to clear the member variable, just to be safe.

Listing 3.27 TRACKER.CPP--RegisterActiveObject Added to the CTracker Constructor

. . .
EnableAutomation();
// make sure that the application won't unload until the reference count is
::AfxOleLockApp();
// clear the member
m_dwRegister = NULL;
// QI for the IUnknown - remember no AddRef
LPUNKNOWN pIUnknown = this->GetInterface(&IID_IUnknown);

// if we have an IUnknown
if(pIUnknown)
{
// register the clsid as an active object so other applications will get the same object
if(::RegisterActiveObject(pIUnknown, CLSID_TRACKER,



ACTIVEOBJECT_STRONG, &m_dwRegister) != S_OK)
// make sure that the reference is clear
m_dwRegister = NULL;
}
// setup our timer resolution
m_lTimeBegin = timeBeginPeriod(1);
m_lHiResTime = m_lLastHiResTime = timeGetTime();

. . .

The last step is to call RevokeActiveObject to remove the server from the Running Object Table. This step is the most critical aspect of shared object support. Do not add this code to the destructor of your server, as it will never be called. The destructor is called in response to the destruction of the server, which results from the server reference count reaching 0. But since the server has an extra reference count from the RegisterActiveObject call, this state is never reached. To ensure that you properly remove the server from the table, your best course of action is to implement the revocation in the Release function of the IUnknown implementation of your server so that the server's reference count can be monitored. Listing 3.28 shows the implementation to revoke the object from the Running Object Table. A call is made to decrement the reference count of the server, and the return value is saved.

The next step is to see if the server has been registered as running, which is implied through a non-zero value in the member variable. Then you check to see if the reference count is 1. If the reference count is 1, the object is ready to be destroyed since the only application now referencing the object is the Running Object Table. Before calling RevokeActiveObject, it is important to increment the reference count and clear the member variable. RevokeActiveObject will result in a recursive call to Release, and you don't want to destroy the object until you are finished with the first call to Release. After the RevokeActiveObject call returns, call the Release function one last time to actually destroy the object and remove it from memory.

Listing 3.28 TRACKER.CPP--RevokeActiveObject Added to the Server

. . .
ULONG FAR EXPORT CTracker::XSubDispatch::Release()
{
METHOD_PROLOGUE(CTracker, SubDispatch)
// call the function and check the refcount
long lRefCount = pThis->ExternalRelease();
// if we are registered as running and there is the only refcount left
if(pThis->m_dwRegister && lRefCount == 1)
{
// bump our refcount up so we don't destroy ourselves until we are done
pThis->ExternalAddRef();
// get the registration ID
DWORD tdwRegister = pThis->m_dwRegister;
// clear the member variable to prevent us from hitting this method again
pThis->m_dwRegister = 0;
// remove the interface from the running object table
::RevokeActiveObject(tdwRegister, NULL);
// the revoke should have decremented our refcount by one
// this call to Release should destroy our server
return pThis->ExternalRelease();
}
// exit
return lRefCount;
}

. . .

During the lifetime of the server, you can get the same instance of the server and use it from multiple applications. In VB, getting a 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 an application creating its own instance of the object. Another approach can be taken: You can supply the instance of a running server to an application that calls CreateObject, rather than relying on an application to call GetObject. This approach is known as a single instance server.

Single Instance Servers

To support single instance servers, it is necessary to perform all of the steps described earlier in this chapter, in the section "Shared Servers," from within the ClassFactory of the server and 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.

Unfortunately, MFC does not provide simple access to the COleObjectFactory class, which is responsible for creating OLE servers to allow for this kind of implementation. C++ inheritance, however, allows you to create a new specialized version of the COleObjectFactory class that can support server instance sharing.

We created the class COleObjectFactoryShared and several macros to speed up your development. As can be seen in Listing 3.29, we created a new class derived from the original MFC class COleObjectFactory. The class contains a constructor and an interface map that routes messages from the IClassFactory and IClassFactory2 interfaces to the implementation. Last is a member variable that is used to store the ID of the server after it has been loaded into the Running Object Table.

Two macros must be added to the server class declaration and implementation to enable shared server support. DECLARE_OLECREATE_SHARED must replace the DECLARE_OLECREATE macro in the class header file, and IMPLEMENT_OLECREATE_SHARED must replace IMPLEMENT_OLECREATE in the server source file. The only difference between the new macros and the originals is that a class factory of type COleObjectFactoryShared will be added to the server implementation rather than a COleObjectFactory class.

Listing 3.29 SHAREDOBJECT.H--Shared Server Class Factory Header File

//
// Shared Server Class Factory
//
// Code based on COleObjectFactory (c) 1996,1997 Microsoft Corp
//
class COleObjectFactoryShared: public COleObjectFactory
{
DECLARE_DYNAMIC(COleObjectFactoryShared)
// Construction
public:
COleObjectFactoryShared(REFCLSID clsid, CRuntimeClass* pRuntimeClass,
BOOL bMultiInstance, LPCTSTR lpszProgID);
// Interface Maps
public:
DECLARE_INTERFACE_MAP()
BEGIN_INTERFACE_PART(SubClassFactory, IClassFactory2)
STDMETHOD(CreateInstance)(LPUNKNOWN, REFIID, LPVOID*);
STDMETHOD(LockServer)(BOOL);
STDMETHOD(GetLicInfo)(LPLICINFO);
STDMETHOD(RequestLicKey)(DWORD, BSTR*);
STDMETHOD(CreateInstanceLic)(LPUNKNOWN, LPUNKNOWN, REFIID, BSTR, LPVOID*);
END_INTERFACE_PART(SubClassFactory)
public:
DWORD m_dwRegister;
};
/////////////////////////////////////////////////////////////////////////////
// Macros for creating "creatable and shareable" automation classes.
#define DECLARE_OLECREATE_SHARED(class_name) \
public: \
static AFX_DATA COleObjectFactoryShared factory; \
static AFX_DATA const GUID guid;
#define IMPLEMENT_OLECREATE_SHARED(class_name, external_name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
AFX_DATADEF COleObjectFactoryShared class_name::factory(class_name::guid, \
RUNTIME_CLASS(class_name), FALSE, _T(external_name)); \
const AFX_DATADEF GUID class_name::guid = \

{ l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } };

Listing 3.30 shows the implementation of the COleObjectFactoryShared class. The constructor defers to the base class and clears the m_dwRegister member variable. As with the previous section, the m_dwRegsiter variable is crucial to the success of the shared server support.

Next you route all messages from the IClassFactory and IClassFactory2 interfaces to the implementation of the functions. All but two of the functions defer to the base class implementation without modification. CreateInstance and CreateInstanceLic first check to see if an instance of the server is already running. If not, the functions attempt to create the server using the base class implementation of CreateInstanceLic; if so, the functions register the server as running. If the server was already running, the function returns the IUnknown of the existing object. Remember that when the server is registered as running, it has an extra reference count. The code to manage the release of the server correctly will remain almost unchanged from the implementation that you created earlier in this chapter.

Listing 3.30 SHAREDOBJECT.CPP--Shared Server Implementation File

//
// Shared Server Class Factory
//
// Code based on COleObjectFactory (c) 1996,1997 Microsoft Corp
//
#include "stdafx.h"
#include "sharedobject.h"
IMPLEMENT_DYNAMIC(COleObjectFactoryShared, COleObjectFactory)
COleObjectFactoryShared::COleObjectFactoryShared(REFCLSID clsid,
CRuntimeClass* pRuntimeClass, BOOL bMultiInstance, LPCTSTR lpszProgID)
:COleObjectFactory(clsid, pRuntimeClass, bMultiInstance, lpszProgID)
{
// clear the registered server ID member
m_dwRegister = NULL;
}
BEGIN_INTERFACE_MAP(COleObjectFactoryShared, COleObjectFactory)
INTERFACE_PART(COleObjectFactoryShared, IID_IClassFactory, SubClassFactory)
INTERFACE_PART(COleObjectFactoryShared, IID_IClassFactory2,
END_INTERFACE_MAP()
/////////////////////////////////////////////////////////////////////////////
// Implementation of COleObjectFactoryShared::IClassFactory interface
STDMETHODIMP_(ULONG) COleObjectFactoryShared::XSubClassFactory::AddRef()
{
METHOD_PROLOGUE_EX_(COleObjectFactoryShared, SubClassFactory)
return pThis->m_xClassFactory.AddRef();
}
STDMETHODIMP_(ULONG) COleObjectFactoryShared::XSubClassFactory::Release()
{
METHOD_PROLOGUE_EX_(COleObjectFactoryShared, SubClassFactory)
return pThis->m_xClassFactory.Release();
}
STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::QueryInterface(
REFIID iid, LPVOID* ppvObj)
{
METHOD_PROLOGUE_EX_(COleObjectFactoryShared, SubClassFactory)
return pThis->m_xClassFactory.QueryInterface(iid, ppvObj);
}
STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::CreateInstance(
IUnknown* pUnkOuter, REFIID riid, LPVOID* ppvObject)
{
METHOD_PROLOGUE_EX_(COleObjectFactoryShared, SubClassFactory)
HRESULT hResult = S_OK;
// Initialize an IUnknown reference
LPUNKNOWN pIUnknown = NULL;

// see if the object is already running
::GetActiveObject(riid, NULL, &pIUnknown);
// if we didn't get a reference to a running object
if(!pIUnknown)
{
// create the server
hResult = pThis->m_xClassFactory.CreateInstanceLic(pUnkOuter,
NULL, riid, NULL, ppvObject);
// if successful
if(hResult == S_OK)
{
// register the clsid as an active object so
// other applications will get the same object
if(::RegisterActiveObject((IUnknown*)*ppvObject, riid,
ACTIVEOBJECT_STRONG, &pThis->m_dwRegister) != S_OK)
// clear the ID member
pThis->m_dwRegister = NULL;
}
else
// clear the ID member
pThis->m_dwRegister = NULL;
}
else
// use the running object
*ppvObject = pIUnknown;
return hResult;
}
STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::LockServer(BOOL fLock)
{
METHOD_PROLOGUE_EX(COleObjectFactoryShared, SubClassFactory)
return pThis->m_xClassFactory.LockServer(fLock);
}
STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::GetLicInfo(
LPLICINFO pLicInfo)
{
METHOD_PROLOGUE_EX(COleObjectFactoryShared, SubClassFactory)
return pThis->m_xClassFactory.GetLicInfo(pLicInfo);
}
STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::RequestLicKey(
DWORD dwReserved, BSTR* pbstrKey)
{
METHOD_PROLOGUE_EX(COleObjectFactoryShared, SubClassFactory)
return pThis->m_xClassFactory.RequestLicKey(dwReserved, pbstrKey);
}
STDMETHODIMP COleObjectFactoryShared::XSubClassFactory::CreateInstanceLic(
LPUNKNOWN pUnkOuter, LPUNKNOWN pUnkReserved, REFIID riid,
BSTR bstrKey, LPVOID* ppvObject)
{
METHOD_PROLOGUE_EX(COleObjectFactoryShared, SubClassFactory)
HRESULT hResult = S_OK;
// clear the ID member
pThis->m_dwRegister = NULL;
// Initialize an IUnknown reference
LPUNKNOWN pIUnknown = NULL;

// see if the object is already running
::GetActiveObject(riid, NULL, &pIUnknown);
// if we didn't get a reference to a running object
if(!pIUnknown)
{
// create the server
hResult = pThis->m_xClassFactory.CreateInstanceLic(pUnkOuter,
pUnkReserved, riid, bstrKey, ppvObject);
// if successful
if(hResult == S_OK)
{
// register the clsid as an active object
// so other applications will get the same object
if(::RegisterActiveObject((IUnknown*)*ppvObject, riid,
ACTIVEOBJECT_STRONG, &pThis->m_dwRegister) != S_OK)
// clear the ID member
pThis->m_dwRegister = NULL;
}
}
else
// use the running object
*ppvObject = pIUnknown;
return hResult;

}

Now that the infrastructure is in place to support shared servers, you need to add the new code to your application.

First the header file must be updated (see Listing 3.31). Replace the macro DECLARE_OLECREATE with the new macro DECLARE_OLECREATE_SHARED. Since the sample application was used for the section on how to implement shared servers, we have also commented out the registered server ID member variable, m_dwRegister.

Listing 3.31 TRACKER.H--Shared Server Class Factory Support Added to the Class Definition

. . .
afx_msg BOOL OutputLines(VARIANT FAR* varOutputArray,
const VARIANT FAR& varIndent);
//}}AFX_DISPATCH
DECLARE_DISPATCH_MAP()
DECLARE_INTERFACE_MAP()
// DECLARE_OLECREATE(CTracker)
DECLARE_OLECREATE_SHARED(CTracker)
// add declaration of ISupportErrorInfo implementation
// to indicate we support the OLE Automation error object
DECLARE_DUAL_ERRORINFO()
HRESULT CreateErrorInfo(CException * pAnyException, REFIID riidSource);
// needed for dual interface support
BEGIN_INTERFACE_PART(SubDispatch, ITracker)
STDMETHOD(GetTypeInfoCount)(THIS_ UINT FAR* pctinfo);
STDMETHOD(GetTypeInfo)(THIS_ UINT itinfo, LCID lcid,
ITypeInfo FAR* FAR* pptinfo);
STDMETHOD(GetIDsOfNames)(THIS_ REFIID riid, OLECHAR FAR* FAR* rgszNames,
UINT cNames, LCID lcid, DISPID FAR* rgdispid);
STDMETHOD(Invoke)(THIS_ DISPID dispidMember, REFIID riid, LCID lcid,
WORD wFlags, DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult,
EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr);
STDMETHOD(get_Indent)(THIS_ long FAR* Value);
STDMETHOD(put_Indent)(THIS_ long Value);
STDMETHOD(OutputLines)(THIS_ VARIANT FAR* varOutputArray,
VARIANT varIndent, VARIANT_BOOL FAR* RetVal);
END_INTERFACE_PART(SubDispatch)
//public:
// DWORD m_dwRegister;
protected:
FILE * m_fileLog;
long m_lTimeBegin;
long m_lHiResTime;
long m_lLastHiResTime;
long m_lIndent;

};

Next you must update the source file for your server (see Listing 3.32). Add the include file SharedObject.h to resolve the macros and class name. In the sample, we commented out the section of code within the constructor that registered the server as running. You must also replace the macro IMPLEMENT_OLECREATE with IMPLEMENT_OLECREATE_SHARED.

Listing 3.32 TRACKER.CPP--Shared Server Update to Class Implementation

. . .
#include "stdafx.h"
#include "MFCServer.h"
// shared object support
#include "sharedobject.h"
// error info support
#include "ErrorInfoMacros.h"
// ODL generated interface file
CTracker::CTracker()
{
EnableAutomation();
// make sure that the application won't unload until the reference count is
::AfxOleLockApp();
// clear the member
/* m_dwRegister = NULL;
// QI for the IUnknown - remember no AddRef
LPUNKNOWN pIUnknown = this->GetInterface(&IID_IUnknown);

// if we have an IUnknown
if(pIUnknown)
{
// register the clsid as an active object so other applications
// will get the same object
if(::RegisterActiveObject(pIUnknown, CLSID_TRACKER, ACTIVEOBJECT_STRONG,
&m_dwRegister) != S_OK)
// make sure that the reference is clear
m_dwRegister = NULL;
}
*/
// 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) oTimeStamp.Format("%Y%m%d"));
// 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"));
}
m_lIndent = 0;
}
. . .

IMPLEMENT_OLECREATE_SHARED(CTracker, _T("MFCServer.Tracker"), 0x11C82947, 0x4edd, 0x11d0, 0xbe, 0xd8, 0x0, 0x40, 0x5, 0x38, 0x97, 0x7d)

The last step is to add the code to revoke the server from the Running Object Table (see Listing 3.33). As with the previous section, the management of the server's lifetime is dependent on its reference count and is handled within the Release function implementation. The only difference between this implementation and the shared object implementation is the location of the m_dwRegister member variable, which is now located in the COleObjectFactoryShared class.

Listing 3.33 TRACKER.CPP--Shared Server Release Implementation

ULONG FAR EXPORT CTracker::XSubDispatch::Release()
{
METHOD_PROLOGUE(CTracker, SubDispatch)
// call the function and check the refcount
long lRefCount = pThis->ExternalRelease();
// if we are registered as running and there is the only refcount left
// if(pThis->m_dwRegister && lRefCount == 1)
if(pThis->factory.m_dwRegister && lRefCount == 1)
{
// bump our refcount up so we don't destroy ourselves until we are done
pThis->ExternalAddRef();
// get the registration ID
// DWORD tdwRegister = pThis->m_dwRegister;
DWORD tdwRegister = pThis->factory.m_dwRegister;
// clear the member variable to prevent us from hitting this method again
// pThis->m_dwRegister = 0;
pThis->factory.m_dwRegister = 0;
// remove the interface from the running object table
::RevokeActiveObject(tdwRegister, NULL);
// the revoke should have decremented our refcount by one
// this call to Release should destroy our server
return pThis->ExternalRelease();
}
// exit
return lRefCount;

}

The implementation and use of shared and single instance server support is straightforward and adds a level of functionality not normally found in standard server implementations.

From Here...

In this chapter, you learned how to create a basic implementation of an MFC Automation Server. You also learned how to expand upon the basic framework provided by MFC to create new and interesting features within your implementation.

Other areas where your server development has room to expand are in adding User Interface, possibly in the form of dialogs and event interfaces. The use of the basic MFC dialog classes can make implementation of UI a very easy and rewarding part of your implementations. At the time of this writing, no container applications will recognize either of these features within an Automation Server. If your implementation has these requirements, you must decide how to implement them. The creation of services and remote servers also makes the prospect of implementing Automation Servers very enticing.

Every day more and more developers are enabling or integrating automation support as a basic feature of their applications. Any situation that calls for the transfer of data among applications can now be performed with OLE automation, whereas before data transfer would have required the exchange of data using DDE or, even worse, file import/export functions.

Automation Servers provide an easy and 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.

Chapter 4 looks at how to create an Automation Server using the ActiveX Template Library (ATL).
Здесь хрестильний