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

Chapter 5
Creating ActiveX Automation Servers Using BaseCtl


Creating ActiveX Automation Servers Using BaseCtl

The history of the BaseCtl framework has its roots in the Visual Basic (VB) group where it was first developed. The single most unique thing about the BaseCtl Framework is its lack of dependence on MFC, which was viewed by the VB team as a burden to creating small and fast OCXs.

Several versions of the BaseCtl framework are floating around. The basic version is the one that ships with the ActiveX SDK, consisting of a number of source files and several samples. A more thorough version, consisting of more samples and even an AppWizard written in VB, has been available to the members of the VB 5 beta testing group for some time.

The BaseCtl framework is intended merely as a sample application and does not have the same support and backing of Microsoft as do its other development products.

In addition to creating ActiveX Controls (see Chapter 8 for more information), the BaseCtl framework can be used to create ActiveX Automation Servers, which are the focus of this chapter. Like ATL, the BaseCtl is not integrated with the VC++ IDE and requires that all code be entered and modified by hand.

In this chapter, you will create a simple in-process Automation Server. As with Chapters 3 and 4, your sample server implementation will be used to log string data to a file.

Creating the Basic Project

Unfortunately, the BaseCtl framework that ships in the ActiveX SDK does not have an AppWizard like its MFC and ATL counterparts. We have provided a sample project based on the BaseCtl samples from which you can create new projects. The sample application that we have created is a bare-bones project similar to the kind that would be generated by an AppWizard. We've included our own sample application because the samples included with the BaseCtl framework are already implemented with specific features and are intended to demonstrate different aspects of BaseCtl development, whereas our sample is meant as a starting point for your own component development.

To create a new project, you first need to define a new directory into which you can copy the sample files; in this case, call the directory BCFServer. Copy all of the files from the BCFBasicServer directory into your new directory, BCFServer. Rename all of the files in the BCFServer directory as described in Table 5.1. The files dispids.h, dwinvers.h, guids.cpp, guids.h, localobj.h, and resource.h will remain as they are.
Table 5.1 New Filenames
Old Filename New Filename
BasicAutoObj.h Tracker.h
BasicAutoObj.cpp Tracker.cpp
BCFBasicServer.h BCFServer.h
BCFBasicServer.cpp BCFServer.cpp
BCFBasicServer.def BCFServer.def
BCFBasicServer.mak BCFServer.mak
BCFBasicServer.odl BCFServer.odl
BCFBasicServer.rc BCFServer.rc

Open the VC++ IDE, and from the File menu select the Find In Files menu item. Look for BCFBasicServer in all of the files in the BCFServer directory. In all of the files that contain the string BCFBasicServer, replace the text with BCFServer. Make sure to do the text replacement on a case-sensitive basis. Repeat the Find In Files search to ensure that you did not miss any files or BCFBasicServer entries.

Next search for the string BasicAutoObj and replace all of the entries with Tracker. Do the same for the string BasicAuto. Remember all text should be replaced on a case-sensitive basis and should be done for all of the files in the new project directory.

The last step is to generate new CLSID that are unique to the project. Use the GUIDGEN.EXE to create the new CLSID, and add them to the ODL file.

After all of the files have been changed, from the File menu, select the Open Workspace menu item. In the Open Workspace dialog, open the BCFServer.mak file. The VC++ IDE will automatically create the file BCFServer.mdp file, which should be used from this point on when opening the project file.

NOTE: At the time of the writing of this book, VC++ 5.0 was still in beta. The file extensions .mak and .mdp have been replaced with the extensions .dsp and .dsw, respectively. Using the older file extensions will in no way affect the sample project or your development.



The last step is to ensure that the location of the BaseCtl include files directory and the path of the BaseCtl libraries are correct. The BCFBasicServer sample assumes that the include directory is established in the Directory tab of the Options dialog, which can be accessed via the Tools menu and the Options menu item. The link settings of the basic project is where the path of the BaseCtl libraries is defined.

Linking Got You Down?

When building your projects utilizing the BaseCtl framework, make sure that you use exactly the same compile settings as those used to create the library. It is important to check all of the settings to ensure that they match exactly. If the compile options do not match, the linker will be unable to create your project and will generate a number of linker errors.



At this point, you can compile the project and even register it in the system registry. However, you can do very little with it since it does not contain any methods or properties.

Like all ActiveX/OLE components, ActiveX Servers are required to support registration and unregistration if they are to be used by other applications.

Registry

The basic registration and unregistration support for the server is already implemented by BaseCtl. Remember that the MFC implementation only allows for registering the server.

Registration support is handled completely by the BaseCtl framework and is hidden from the developer. The registration information is part of the DEFINE_AUTOMATIONOBJECT structure (see Listing 5.1). See the BaseCtl documentation and samples for information about the structure and its content.

Listing 5.1 TRACKER.H--Basic Registration Information in the DEFINE_AUTOMATIONOBJECT Structure

// TODO: modify anything appropriate in this structure, such as the helpfile
// name, the version number, etc.
//
DEFINE_AUTOMATIONOBJECT(Tracker,
&CLSID_Tracker,
"Tracker",
CTracker::Create,
1,
&IID_ITracker,


"Tracker.Hlp");

Sample Server Support Code

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

Listing 5.2 shows the additions that are made to the class header file. First you added two new include files that are needed to support the file and timer functions that the server uses. A set of member variables is added for storing the file handle and timer information that will be used throughout the server implementation.

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

//
#ifndef _TRACKER_H_

#include "AutoObj.H"
#include "BCFServerInterfaces.H"
// needed for FILE services
#include <stdio.h>
// needed for the high resolution timer services
#include <mmsystem.h>

class CTracker : public ITracker, public CAutomationObject, ISupportErrorInfo {

public:

. . .

protected:
virtual HRESULT InternalQueryInterface(REFIID riid, void **ppvObjOut);

private:
// member variables that nobody else gets to look at.
// TODO: add your member variables and private functions here.

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


};

In the sample implementation, the constructor and destructor implementations are updated to open and close the log file (see Listing 5.3). First a high resolution timer is created and its current value is stored in the member variables. The timer is useful for determining the number of milliseconds that have passed since the last method call was made. This 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 startup data to the file and exit the constructor.

The destructor does the exact opposite of the constructor. If you have a valid file handle, you write some closing information to the file and close it. Next you terminate the timer.

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

#pragma warning(disable:4355) // using `this' in constructor
CTracker::CTracker
(
IUnknown *pUnkOuter
)
: CAutomationObject(pUnkOuter, OBJECT_TYPE_OBJTRACKER, (void *)this)
{

// setup our timer resolution
m_lTimeBegin = timeBeginPeriod(1);
m_lHiResTime = m_lLastHiResTime = timeGetTime();

SYSTEMTIME sTime;
// get the current date and time
::GetLocalTime(&sTime);

TCHAR tstrFileName[18];
// format the file name based on the current date
sprintf(&tstrFileName[0], "%04d%02d%02d.tracklog",
sTime.wYear, sTime.wMonth, sTime.wDay);

// open a file
m_fileLog = fopen(&tstrFileName[0], "a");

// if we have a file handle
if(m_fileLog)
{
// output some starting information
fprintf(m_fileLog, "************************\n");
fprintf(m_fileLog, "Start %02d/%02d/%04d - %02d:%02d:%02d\n",
sTime.wMonth, sTime.wDay, sTime.wYear,
sTime.wHour, sTime.wMinute, sTime.wSecond);
fprintf(m_fileLog, "\n");
}
}
#pragma warning(default:4355) // using `this' in constructor

//=--------------------------------------------------------------------------=
// CTracker::CTracker
//=--------------------------------------------------------------------------=
// "We all labour against our own cure, for death is the cure of all diseases"
// - Sir Thomas Browne (1605 - 82)
//
// Notes:
//
CTracker::~CTracker ()
{
// if we have a file handle
if(m_fileLog)
{
SYSTEMTIME sTime;
// get the current date and time
::GetLocalTime(&sTime);

// output some closing information
fprintf(m_fileLog, "\n");
fprintf(m_fileLog, "End %02d/%02d/%04d - %02d:%02d:%02d\n",
sTime.wMonth, sTime.wDay, sTime.wYear,
sTime.wHour, sTime.wMinute, sTime.wSecond);
fprintf(m_fileLog, "************************\n");

// close the file
fclose(m_fileLog);
}


}

The last thing you need to do is update the build settings for the project. Since the sample implementation is using some timer functions defined in mmsystem.h, you need to link with the appropriate library file that contains their implementation. Under the Project menu, select the Settings menu item. In the Project Settings dialog 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 has been 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. 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, because 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.

All work involving methods and properties in BaseCtl (and also ATL) starts with the ODL (or IDL) file. Methods and properties are declared here, first, and then implemented in the server.

BaseCtl Automation Servers are dual-interface by default, so you must declare all of the methods and properties so as to conform to standard dual-interface rules. Refer to Chapters 3 and 4 for more information on dual-interface.

Listing 5.4 shows the declaration of the OutputLines method.

OutputLines is defined as having two parameters: varOutputArray, as a VARIANT passed by reference that will contain a string array of data to output to the file, and varIndent, as a VARIANT passed by value, which is also an optional parameter indicating the amount of indentation when writing the string data to the file. The third parameter is actually the return type of the method and is defined as a Boolean.

See Chapter 3 regarding the use of Boolean data types and the differences between VB and VC++.

Due to data type restrictions imposed by OLE 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. varIndent is an optional parameter that indents your text output as an added formatting feature (see Listing 5.4).

Listing 5.4 BCFSERVER.ODL--OutputLines Method ODL Declaration

. . .
[
uuid(5ea29be0-5a82-11d0-bcbc-0020afd6738c),
helpstring("Tracker Object"),
hidden,
dual,
odl
]
interface ITracker : IDispatch {

// properties
//

// methods
[id(2)] HRESULT OutputLines([in] VARIANT * varOutputArray,
[in, optional] VARIANT varIndent, [out, retval] boolean * RetVal);
};

// coclass for CTracker objects
//


. . .

When the ODL file is compiled into a type library, the compiler will create a header file that contains all of the interface and CLSID declarations in C++ format. It is from this file that you will copy all of the function prototypes that are needed in your server implementation for defining the methods and properties that it contains. From the file BCFServerInterfaces.h, copy the OutputLines function prototype, and paste it into the class definition of the server (see Listing 5.5). Remember to remove the PURE keyword from the function prototype.

For the purposes of the sample implementation, you also need to add the m_lIndent member variable, which is used in the OutputLines method implementation, and later as a property of the server.

Listing 5.5 TRACKER.H--OutputLines Function Prototype Added to the Class Definition


. . .

CTracker(IUnknown *);
virtual ~CTracker();

// ITracker methods
STDMETHOD(OutputLines)(VARIANT FAR* varOutputArray, VARIANT varIndent,
VARIANT_BOOL FAR* RetVal);

// creation method
//
static IUnknown *Create(IUnknown *);

protected:
virtual HRESULT InternalQueryInterface(REFIID riid, void **ppvObjOut);

private:
// member variables that nobody else gets to look at.
// TODO: add your member variables and private functions here.

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


. . .

Before adding the OutputLines implementation, it is necessary to update the constructor to initialize the m_lIndent member variable to a valid state (see Listing 5.6).

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


. . .

// output some starting information
fprintf(m_fileLog, "************************\n");
fprintf(m_fileLog, "Start %02d/%02d/%04d - %02d:%02d:%02d\n",
sTime.wMonth, sTime.wDay, sTime.wYear,
sTime.wHour, sTime.wMinute, sTime.wSecond);
fprintf(m_fileLog, "\n");
}

m_lIndent = 0;
}


#pragma warning(default:4355) // using `this' in constructor

Next you add the OutputLines implementation to the source file as in Listing 5.7. The implementation varies very little from the ATL sample.

As with the MFC and ATL implementations, the BaseCtl version checks the array parameter to ensure its validity and, if so, outputs the data to the file, indenting the text if appropriate. See Chapter 3 for more information regarding the other implementation details.

For now, the implementation returns VARIANT_FALSE in the cases where an error has occurred. Later in this chapter, you will learn how to create rich error information.

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


STDMETHODIMP CTracker::OutputLines(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal)
{
HRESULT hResult = S_OK;

*RetVal = VARIANT_TRUE;

// if we have a file and 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)
{
SYSTEMTIME sTime;
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;

// for each of the elements in the array
for(long lArrayCount = lLBound; lArrayCount <
(lUBound + lLBound); lArrayCount++)
{
// get the current date and time
::GetLocalTime(&sTime);
m_lHiResTime = timeGetTime();

// get the data from the array
if(::SafeArrayGetElement(varOutputArray->parray,
&lArrayCount, &bstrTemp) == S_OK)
{
// output the data
fprintf(m_fileLog, "%02d:%02d:%02d(%10ld)-",
sTime.wHour, sTime.wMinute, sTime.wSecond, m_lHiResTime - m_lLastHiResTime);

// if we have to indent the text
for(long lIndentCount = 0; lIndentCount < m_lIndent;
lIndentCount++)
// add a tab to the string
fprintf(m_fileLog, "\t");

// output the data
fprintf(m_fileLog, "%ls\n", bstrTemp);

// store the last timer value
m_lLastHiResTime = m_lHiResTime;

// free the bstr
::SysFreeString(bstrTemp);
}
}
}
else
*RetVal = VARIANT_FALSE;
}
else
*RetVal = VARIANT_FALSE;

// unlock the array we don't need it anymore
::SafeArrayUnlock(varOutputArray->parray);


}
else


*RetVal = VARIANT_FALSE;
}
else
*RetVal = VARIANT_FALSE;

// return the result
return hResult;


}

Now that you have added a method, you will learn how to implement its counterpart, the Property.

Adding Properties

A property can be thought of as an exposed variable that is defined in the automation server. Properties are useful for setting and retrieving information about the state of the server.

The m_lIndent member variable that you added to the class definition is a perfect candidate to be exposed as a property.

As with methods, properties are also added to a server via the ODL file. Listing 5.8 shows the ODL declaration for the Indent property. As stated in Chapters 3 and 4, properties are implemented as a pair of methods, one to get the value and the other to set the value. Since the server is dual-interface by default, the methods must also conform to dual-interface declaration rules.

Listing 5.8 BCFSERVER.ODL--Indent Property Added to the ODL

. . .

interface ITracker : IDispatch {

// properties
[id(1), propget] HRESULT Indent([out, retval] long * Value);
[id(1), propput] HRESULT Indent([in] long Value);

// methods
[id(2)] HRESULT OutputLines([in] VARIANT * varOutputArray,
[in, optional] VARIANT varIndent, [out, retval] boolean * RetVal);
};



. . .

Compile the ODL file, and copy the function prototypes from the BCFServerInterfaces.h file to the Tracker class definition (see Listing 5.9). Don't forget to remove the PURE keyword from the new function declarations.

Listing 5.9 TRACKER.H--Indent Property Function Prototypes Added to the Class Definition


. . .

CTracker(IUnknown *);
virtual ~CTracker();

// ITracker methods
STDMETHOD(get_Indent)(long FAR* Value);
STDMETHOD(put_Indent)(long Value);
STDMETHOD(OutputLines)(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal);

// creation method
//
static IUnknown *Create(IUnknown *);



. . .

The actual implementation of the Indent property is very simple (see Listing 5.10). Get_Indent returns the value currently stored in the member variable, and Put_Indent stores the new value, after a little bit of error checking, in the member variable.

Listing 5.10 TRACKER.CPP--Indent Property Implementation


STDMETHODIMP CTracker::get_Indent(long FAR *Value)
{
HRESULT hResult = S_OK;

// return the member variable
*Value = m_lIndent;

// return the result
return hResult;
}

STDMETHODIMP CTracker::put_Indent(long Value)
{
HRESULT hResult = S_OK;

// if the new value is a least 0
if(Value >= 0)
// assign the value to our member variable
m_lIndent = Value;

// return the result
return hResult;


}

Properties, like methods, also have a wide variety of implementation options, including parameterized and enumerated values. See Chapters 6 through 11 on developing ActiveX controls for descriptions of more options and features when creating properties.

You've added methods and properties to the server, but you haven't really dealt with the issue of error handling in their implementation. In some cases, simply returning success or failure is not enough information for the developer to understand that an error occurred and what caused it. You will communicate more error information through the use of OLE exceptions.

Generating OLE Exceptions

While executing a method call or some other action, it is necessary at times 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. This 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 that are confined to the application in which they are defined. The second is an OLE exception. OLE exceptions are used to communicate the same kinds of errors externally to applications that are using a component. The difference between the two is that C++ exceptions are used internally to an application's implementation and OLE exceptions are used externally to communicate errors to other applications.

The COM implementation of a method in a Dual-Interface Server does not have the same kind of error management features that IDispatch interfaces have. To generate the proper error information, an application must use the IErrorInfo object, which is provided by the operating system. A server need only support the ISupportErrorInfo interface, which lets an automation controller know that it should look at the IErrorInfo object for more information when an error occurs.

The first step is to add an enumeration of the types of errors that the server can generate to the ODL file (see Listing 5.11). This step has the effect of publishing the error constants to the user of the automation server. The enumeration can be added anywhere within the ODL file and still produce the proper C++ declaration in the BCFServerInterfaces.h file. Remember to generate a new CLSID for the enumeration.

Listing 5.11 BCFSERVER.ODL--Error Enumeration in the ODL File


coclass Tracker {
[default] interface ITracker;
};

typedef [uuid(AA3DFE23-5C1C-11d0-BEE7-00400538977D),
helpstring("Tracker Error Constants")]
enum tagTrackerError
{
MFCSERVER_E_NO_UBOUND = 46080,
MFCSERVER_E_NO_LBOUND = 46081,
MFCSERVER_E_NO_ARRAYLOCK = 46082,
MFCSERVER_E_NO_FILE = 46083,
MFCSERVER_E_BAD_ARRAY_PARAMETER = 46084,
MFCSERVER_E_INVALID_VALUE = 46085
}TRACKERERROR;


};

The next step is to add the actual error generating code. Listing 5.12 shows the additional code that is added to the server implementation of OutputLines and put_Indent to enable error generation.

BaseCtl provides a helper function Exception for generating rich error information. This function takes three parameters: an error number, a string resource ID, and a help context ID. See Chapters 3 and 4 for more information regarding rich error information.

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


STDMETHODIMP CTracker::OutputLines(VARIANT FAR* varOutputArray, VARIANT varIndent, VARIANT_BOOL FAR* RetVal)
{
HRESULT hResult = S_OK;

*RetVal = VARIANT_TRUE;

// if we have a file and 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)
{
. . .
else
{
*RetVal = VARIANT_FALSE;



// create the error message
hResult = this->Exception(
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF,
MFCSERVER_E_NO_UBOUND),
IDS_E_NO_UBOUND,
NULL);
}
}
else
{
*RetVal = VARIANT_FALSE;

// create the error message
hResult = this->Exception(
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF,
MFCSERVER_E_NO_LBOUND),
IDS_E_NO_LBOUND,
NULL);
}

// unlock the array we don't need it anymore
::SafeArrayUnlock(varOutputArray->parray);
}
else
{
*RetVal = VARIANT_FALSE;

// create the error message
hResult = this->Exception(
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_ARRAYLOCK),
IDS_E_NO_ARRAYLOCK,
NULL);
}
}
else
{
*RetVal = VARIANT_FALSE;

// if there wasn't a file
if(!m_fileLog)
// create the error message
hResult = this->Exception(
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_FILE),
IDS_E_NO_FILE,
NULL);
else
// create the error message
hResult = this->Exception(
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF,
MFCSERVER_E_BAD_ARRAY_PARAMETER),
IDS_E_BAD_ARRAY_PARAMETER,
NULL);
}

// return the result
return hResult;
}

STDMETHODIMP CTracker::get_Indent(long FAR *Value)
{
HRESULT hResult = S_OK;

// return the member variable
*Value = m_lIndent;

// return the result
return hResult;
}

STDMETHODIMP CTracker::put_Indent(long Value)
{
HRESULT hResult = S_OK;

// if the new value is at least 0
if(Value >= 0)
// assign the value to our member variable
m_lIndent = Value;
else
{
// create the error message
hResult = this->Exception(
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_INVALID_VALUE),
IDS_E_INVALID_VALUE,
NULL);
}

// return the result
return hResult;


}

Take care when defining your interfaces and how they communicate errors and invalid conditions. The use of rich error information is very helpful to the user of the component and can be as critical to the implementation as the methods and properties that the server supports.

Dual-Interface

In Chapter 3, the basic MFC server is implemented as IDispatch only, and you are required to add dual-interface support as an extra step. With BaseCtl, dual-interface support is built in and implemented as a normal aspect of the server.

Generating Dual-Interface OLE Exceptions

Again as in Chapter 3, the basic MFC server is implemented as IDispatch only, and it is possible to throw standard C++ exceptions and have the basic MFC IDispatch support code translate the error into an OLE exception. When an MFC server is converted to dual-interface, you must implement the exception translation code yourself.

For BaseCtl, the server has been implemented as dual-interface from the start, and all error generation has been written as true OLE exceptions and does not require translation as in MFC.

Server Instantiation Using C++

OLE is not the only method for creating and using automation servers. This chapter will show you how to create OLE servers using C++ syntax.

At times, creating 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 were created via OLE.

For an MFC server, the inclusion or exclusion of the macros DECLARE_OLECREATE and IMPLEMENT_OLECREATE determined whether a server is creatable by external applications. For BaseCtl, it is a little simpler. All BaseCtl applications contain a global variable called ObjectInfo for declaring all of the servers that can be created via OLE. The ObjectInfo structure is declared in the main application file:


OBJECTINFO g_ObjectInfo[] = {
EMPTYOBJECT


};

Each OLE server implemented within the application will have a single entry within the body of the ObjectInfo structure, thus identifying it as an exposed OLE server. The AUTOMATIONOBJECT macro defines the C++ class of the server that can be created:


OBJECTINFO g_ObjectInfo[] = {
AUTOMATIONOBJECT(Tracker),
EMPTYOBJECT


};

To prevent an application from being exposed as an automation server, you only need to remove or comment out the entry in the object map. On the opposite side, when adding additional creatable servers to an application, it is important to add an entry for each new server to the ObjectInfo structure.

All BaseCtl servers contain a static function Create that is used to create instances of themselves. The Create function is the only way that you should instantiate a server since it is implemented by the class factory of the server and will manage the server creation correctly. This is critical in the cases where the server is shared among two or more applications, as you will see in the next sections.

Listing 5.13 shows an example of creating a server using C++ syntax. Create returns an IUnknown *, which is used to retrieve the proper custom interface that can then be cast to the C++ class of the server. Remember to check all of the return values and pointers to ensure that all of the method calls returned successfully. Don't forget to increment and decrement the reference counts of the server to ensure that its lifetime is what you expect. You must also navigate to the correct interface using the function QueryInterface, as is the case in Listing 5.13, which is looking for the custom interface pointer of the IID_ITracker object. A simple cast from the Create function IUnknown * to the C++ class is not enough to get the correct function offsets.

Listing 5.13 Example--Creating an OLE Server Using C++

// create the server
IUnknown * pUnk = CTracker::Create(NULL);

// if we have an IUnknown
if(pUnk)
{
// custom interface reference
ITracker * pTrack = NULL;

// QI for the custom interface and if successful
if(pUnk->QueryInterface(IID_ITracker, (LPVOID *) &pTrack) == S_OK)
{
// too many ref counts - release one
pTrack->Release();

// object reference
CTracker * opTracker = NULL;

// cast to the object
opTracker = (CTracker *) pTrack;

// use the object
opTracker->put_Indent(1);

// release the object - this is the last one so the
// object should be destroyed
opTracker->Release();
}
else
// release the IUnknown - we couldn't find the custom interface
pUnk->Release();


}

After a server has been created this way, it can be used like any other C++ class or OLE server. Since the server is a running OLE Object, all of the standard OLE functions work, and the server can be used from other applications; for example, QueryInterface can be used to retrieve IDispatch or custom interface pointers that can be passed to other applications. See Chapter 3 for more information regarding the creation and use of OLE servers with C++.

So far, you've looked only at how to create individual instances of objects. Next you will find out how to share objects.

Shared Servers

OLE defines a facility, called the Running Object Table, for sharing objects. An object that is shareable 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. This capability is useful for applications that may need to work with a single 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 is to register the server as running (see Listing 5.14). The BaseCtl Framework allows access to the basic ClassFactory operation of creating the server through the static Create function. Since this function is where all instances of the server are created, the Create function is the most logical location to register the server as running.

After the new object is created, you should QueryInterface for an IUnknown reference to the server. If successful, you call the RegisterActiveObject passing the IUnknown and the CLSID of the server. This sample implementation passes the constant ACTIVEOBJECT_STRONG, which will result in the reference count of the server being incremented by one. See the VC++ documentation for more information regarding this parameter. You must also pass the address of a member variable to store the ID of the running object. The ID is used later when revoking the object from the Running Object Table. Last you must decrement the reference count of the server to offset the QueryInterface call.

Listing 5.14 TRACKER.CPP--Create Function Implementation of Shared Object Support

IUnknown *CTracker::Create
(
IUnknown *pUnkOuter
)
{
// make sure we return the private unknown so that we support aggegation
// correctly!
//
CTracker *pNew = new CTracker(pUnkOuter);

LPUNKNOWN pIUnknown = NULL;

// QI for the IUnknown and if successful
if(pNew->QueryInterface(IID_IUnknown, (LPVOID *) &pIUnknown) == S_OK)
{
// register the clsid as an active object
// so other applications will get the same object
// and if it didn't succeed - this function will
// increment the reference count because of the ACTIVEOBJECT_STRONG
if(::RegisterActiveObject(pIUnknown, CLSID_Tracker,
ACTIVEOBJECT_STRONG, &pNew->m_dwRegister) != S_OK)
// make sure that the reference ID is clear
pNew->m_dwRegister = NULL;

// remove the extra refcount from the QI
pIUnknown->Release();
}

return pNew->PrivateUnknown();


}

You've added the code to register the server in the Running Object Table, but now you need to add code to remove the server from the table. The only practical way to remove the server from the Running Object Table is to monitor the reference counts of the server. When the reference count reaches one--indicating that the only reference left is the Running Object Table's reference--revoke the server's reference from the Running Object Table. The only location that you can use to accurately monitor the reference count of the server is in the IUnknown::Release function implementation.

To aid in the implementation of the IUnknown::Release function, we have supplied a new macro to handle the revoking the server from the Running Object Table (see Listing 5.15). The implementation checks to see whether an ID is present for the server and whether the reference count is one and revokes the server from the Running Object Table. The implementation also increments the reference count and clears the ID member variable to prevent the object from being destroyed before this function has completed and also to prevent the code from becoming recursive. Incrementing the reference count and clearing the member variable is necessary since the RevokeActiveObject function call will result in another call to Release to decrement the reference of the server. After RevokeActiveObject returns, the server removes its last reference count, which results in the destruction of the server.

Listing 5.15 RACKER.H--DECLARE_STANDARD_UNKNOWN_SHARED Macro Implementation


#define DECLARE_STANDARD_UNKNOWN_SHARED() \
STDMETHOD(QueryInterface)(REFIID riid, void **ppvObjOut) { \
return ExternalQueryInterface(riid, ppvObjOut); \
} \
STDMETHOD_(ULONG, AddRef)(void) { \
return ExternalAddRef(); \
} \
STDMETHOD_(ULONG, Release)(void) \
{ \
long lRefCount = this->ExternalRelease(); \
if(this->m_dwRegister && lRefCount == 1) \
{ \
this->ExternalAddRef(); \
DWORD tdwRegister = this->m_dwRegister; \
this->m_dwRegister = 0; \
::RevokeActiveObject(tdwRegister, NULL); \
return this->ExternalRelease(); \
} \
else \
return lRefCount; \
} \
DWORD m_dwRegister;

The last step is to update the class declaration of your server to use the new macro (see Listing 5.16). Replace the DECLARE_STANDARD_UNKNOWN() with DECLARE_STANDARD_UNKNOWN_SHARED().

Listing 5.16 TRACKER.H--New Macro

class CTracker : public ITracker, public CAutomationObject, ISupportErrorInfo {
public:
// IUnknown methods
//
DECLARE_STANDARD_UNKNOWN_SHARED();
// IDispatch methods
//
DECLARE_STANDARD_DISPATCH();
. . .

During the lifetime of the server, an application is capable of getting the instance of the running server with a method call to OLE. In VB, you use the GetObject method call, and in VC++, you use the GetActiveObject function. After the pointer to the server is retrieved, the server can be used as though it were created through normal OLE mechanisms.

This method of sharing objects is fine but requires that the developer of the application using the server take an active role in deciding to use the shared object versus creating a new instance of the object. Another approach can be taken, and that is to supply the instance of a running server to an application that calls CreateObject rather than GetObject. A server implemented to always return the same instance to any application is referred to as a single instance server.

Single Instance Servers

A single instance server is used in the cases where you do not want the users of the component to have a choice in using a shared instance of the server or a copy that they create themselves. No matter what, you want to create only a single running instance of the server that is shared by all applications.

To enable a BaseCtl server for single instance requires that you modify the code that was added to the Create function in the section "Shared Servers." Listing 5.17 shows the changes that you made to the sample implementation to enable single instance support. First, you must look to see whether the server is already registered as running. If not, a server is created and registered as running.

If the server is already running, you must QueryInterface for the custom interface pointer so that you can cast to the correct C++ class type. The last step is to call Release since there will be an extra reference count from the GetActiveObject and QueryInterface calls.

Listing 5.17 TRACKER.CPP--Create Function Implementation Updated to Support Single Instance Servers


IUnknown *CTracker::Create
(
IUnknown *pUnkOuter
)
{
CTracker *pNew = NULL;

// Initialize an IUnknown reference
LPUNKNOWN pIUnknown = NULL;

// see if the object is already running
::GetActiveObject(CLSID_Tracker, NULL, &pIUnknown);

// if we didn't get a reference to a running object
if(!pIUnknown)
{
// make sure we return the private unknown so that we support // aggegation correctly!
//
pNew = new CTracker(pUnkOuter);

// QI for the IUnknown and if successful
if(pNew->QueryInterface(IID_IUnknown, (LPVOID *) &pIUnknown) == S_OK)
{
// register the clsid as an active object
// so other applications will get the same object
// and if it didn't succeed - this function will
// increment the reference count because of the ACTIVEOBJECT_STRONG
if(::RegisterActiveObject(pIUnknown, CLSID_Tracker,
ACTIVEOBJECT_STRONG, &pNew->m_dwRegister) != S_OK)
// make sure that the reference ID is clear
pNew->m_dwRegister = NULL;

// remove the extra refcount from the QI
pIUnknown->Release();
}
}
else
{
// custom interface reference
ITracker * pTrack = NULL;

// QI for the custom interface and if successful
if(pIUnknown->QueryInterface(IID_ITracker, (LPVOID *) &pTrack) == S_OK)
{
// too many ref counts - release one
pTrack->Release();

// cast to the object
pNew = (CTracker *) pTrack;
}
else
{
// release the IUnknown - we couldn't find the custom interface
pIUnknown->Release();

// go ahead and create an object
pNew = new CTracker(pUnkOuter);

// QI for the IUnknown and if successful
if(pNew->QueryInterface(IID_IUnknown, (LPVOID *) &pIUnknown) == S_OK)
{
// register the clsid as an active
// object so other applications will get the same object
// and if it didn't succeed - this function will
// increment the reference count because of the ACTIVEOBJECT_STRONG
if(::RegisterActiveObject(pIUnknown, CLSID_Tracker,
ACTIVEOBJECT_STRONG, &pNew->m_dwRegister) != S_OK)
// make sure that the reference ID is clear
pNew->m_dwRegister = NULL;

// remove the extra refcount from the QI
pIUnknown->Release();
}
}
}
return pNew->PrivateUnknown();
}

Shared server support is very straightforward to implement and use and adds a level of functionality not normally available to standard server implementations.

User Interface and Events

Like MFC and ATL, BaseCtl servers can also benefit from the addition of User Interface (UI) and Events. Again, since BaseCtl does not offer integration with MFC, you will be forced to implement all of the UI with the standard Win32 functions, which is not a very appealing prospect. At the time this book was written, only Visual Basic 5 was capable of using event interfaces in automation servers.

From Here...

In this chapter, you created a simple implementation of a BaseCtl automation server. You also learned how to expand upon the basic framework provided by BaseCtl to create new and interesting features within your implementation. BaseCtl provides a simple and straightforward framework for creating Automation Servers. Unfortunately, the lack of MFC integration and support from Microsoft as a development platform will put it last on your list of choices when selecting a development direction.

For those of you who are willing to or interested in learning more about Automation Servers and ActiveX architecture, the BaseCtl is a fine way to learn since it is so lean. The BaseCtl is probably better in this area than ATL because you won't have to understand the concept of templates while also learning ActiveX.

The next six chapters take a look at implementing ActiveX controls using the same three tool sets: MFC, ATL, and BaseCtl.