Initialization

At some point in executing a script, a controller will have to create the automation object that it wants to drive. This usually happens through CoCreateInstance, but it might occur through other means, such as binding a file moniker. For whatever reason, the controller will want to obtain information about the object and its environment for use when invoking methods and properties.

AutoCli instantiates the currently installed Beeper object during startup (after calling CoInitialize, of course). This example is somewhat contrived—AutoCli runs one script, which creates the object on startup, invokes methods and properties based on menu selections, and then destroys the object on shutdown. Controllers that are programming tools will generally create, manipulate, and destroy objects while running a script. Other controllers might perform the same action in response to various user commands. No matter how the controller is designed, it must create the object at some point, as AutoCli does in CApp::Init (AUTOCLI.CPP):


hr=CoCreateInstance(CLSID_Beeper, NULL, CLSCTX_INPROC_SERVER
, IID_IDispatch, (PPVOID)&m_pIDispatch);

If this call fails, AutoCli displays a message and terminates. Obviously, this isn't the most friendly way for an application to behave, but because AutoCli cannot continue without a Beeper, it is appropriate here. A more general purpose controller would encounter this sort of error only when trying to execute a script, in which case it would inform the user of the error and perhaps give possible solutions to the problem.

After successfully creating the object, AutoCli loads the object's HELPDIR value from its TypeLib registry entries. (This is the directory in which the object installed any help files that might be named in an exception.) The HelpDirFromCLSID function accomplishes this; it is written to be a stand-alone piece of code that you can cut and paste.


void HelpDirFromCLSID(CLSID clsID, LPTSTR pszPath)
{
TCHAR szCLSID[80];
TCHAR szKey[512];
UINT cch;
long lRet;

if (NULL==pszPath)
return;

*pszPath=0;

cch=sizeof(szCLSID)/sizeof(TCHAR);
StringFromGUID2(clsID, szCLSID, cch);
wsprintf(szKey, TEXT("CLSID\\%s\\TypeLib"), szCLSID);

//Get LIBID from under CLSID.
if (ERROR_SUCCESS==RegQueryValue(HKEY_CLASSES_ROOT, szKey
, szCLSID, &lRet))
{
//Get HELPDIR from under TypeLib.
wsprintf(szKey, TEXT("TypeLib\\%s\\HELPDIR"), szCLSID);
RegQueryValue(HKEY_CLASSES_ROOT, szKey, pszPath, &lRet);
}

return;
}

In your own code, you'll probably want the object's type information, at least for its dispinterface. Although AutoCli doesn't demonstrate this, a quick call to IDispatch will get you the ITypeInfo you want:


ITypeInfo *pITypeInfoDispInt;
UINT cTypeInfo;

if (SUCCEEDED(pIDispatch->GetTypeInfoCount(&cTypeInfo)))
pIDispatch->GetTypeInfo(0, lcid, &pITypeInfoDispInt);

If you want to get the object's type information for the purpose of looking at both its incoming and outgoing interfaces, you can try using the IProvideClassInfo interface:


IProvideClassInfo   *pIPCI;
ITypeInfo *pITypeInfoObject;

if (SUCCEEDED(pIDispatch->QueryInterface(IID_IProvideClassInfo
, (void**)&pIPCI)))
{
pIPCI->GetClassInfo(&pITypeInfoObject);
pIPCI->Release();
}

Another alternative is to use ITypeInfo::GetContainingTypeLib and ITypeInfo::GetTypeInfoOfGUID, which would look like the following, assuming you have pITypeInfoDispInt from the code above:


ITypeLib     *pITypeLib;
ITypeInfo *pITypeInfoObject;

if (SUCCEEDED(pITypeInfoDispInt->GetContainingTypeLib(&pITypeLib, 0)))
{
pITypeLib->GetTypeInfoOfGUID(CLSID_Beeper, &pITypeInfoObject);
pITypeLib->Release();
}

There is one more little snag: in the previous call to IDispatch::GetTypeInfo is that little lcid parameter. We need a locale to pass to most of the IDispatch functions, especially GetIDsOfNames. With AutoCli, we always use basic English with LANGID_ENGLISH and SUBLANGID_NEUTRAL. The LCID for this is stored in the variable m_lcid in the application's constructor, CApp::CApp, and is passed later to IDispatch members.

AutoCli uses English because the method and property names that it employs elsewhere are hard-coded for English. Obviously, this isn't the best solution for a general purpose controller.

A better solution is to load the current user language with GetUserDefaultLCID. This is best for creating new scripts for the controllers; the user probably wants to work in his or her national language whenever possible. You might also want to allow some way for the user—who may be a developer—to specify the use of a neutral language, especially if that person is writing a script that is expected to be executed in different countries using different languages.

In either case, when a controller saves a script, it should also save the LCID under which a script was written because it is likely that the names of methods and properties saved in that script are also expressed in that language. This would allow the controller running under a different language to check whether type information for the original language is available when the script is loaded—a much better solution than telling the user that a name cannot be resolved at run time because it's in the wrong language (which isn't necessarily known at run time).

Now that we have the information necessary for driving an object, we can invoke the object's methods and properties. When we're finished with the object, we simply call IDispatch::Release, and the object takes care of deleting itself and shutting down its application or unloading its DLL as necessary. And when our program shuts down, it calls CoUninitialize as usual.