Handle Exceptions

Now that you understand the different scenarios for making calls to IDispatch::Invoke, we can look at what happens after Invoke returns. If a call returns NOERROR, everything worked, and you can use the contents of the VARIANT that contains the value of a property get or the return value of a method call. In addition, you can use whatever values might be contained in out-parameters.

Invoke, of course, can return many different errors, for example, DISP_E_TYPEMISMATCH. For any error other than DISP_E_EXCEPTION, you should have the controller display meaningful error messages and give the user guidance as to how to fix the problem.

If a call returns DISP_E_EXCEPTION, the object itself has chosen to provide you with the necessary information about the problem in the EXCEPINFO structure you passed it. (Of course, if you didn't pass this structure, you cannot obtain exception information.) A controller that receives this information and wants to display it to the user should perform the following steps; each field mentioned is part of EXCEPINFO:

Check whether the pfnDeferredFillIn is non-NULL. If so, call that function passing your EXCEPINFO structure again.3

Format a message string containing the source of the error (the value of the ProgID in bstrSource), the error code (either wCode or scode), and the description of the exception (bstrDescription). A typical format is "Error <code> in <source name>: <description>". The scode field is valid only when wCode is 0, and it is best to format wCode in decimal but scode in hexademical. As you probably know by now, SCODEs are far easier to read in hex.

Display the message in a message box with at least the MB_OK and MB_ICONEXCLAMATION styles, as shown in Figure 15-2. If the bstrHelpFile field is non-NULL, however, also include a Help button. Under Windows 3.1 and Windows NT 3.5, doing this requires that you make your own dialog box template. Blech! Under Windows 95 and later versions of Windows NT, you can use a new style, MB_HELP, which gets MessageBox to include the Help button.

If the user clicks the Help button, you must launch WinHelp with the full pathname for bstrHelpFile along with the context ID dwHelpContext. This is where the HELPDIR we read from the registry earlier in this chapter comes into play. If we are able to read a help directory, we can assume that the object does not put a full path in the bstrHelpFile field. Therefore, we must prepend HELPDIR to bstrHelpFile to form the path to send to WinHelp. If we were not able to read a directory name earlier, we can only assume that the object has put the full path in bstrHelpFile and should send it unmodified to WinHelp.

Free bstrSource, bstrDescription, and bstrHelpFile with calls to SysFreeString.

Figure 15-2.

A typical controller display of an exception.

In AutoCli, most of the code in the helper function CApp::Invoke is for the single purpose of handling exceptions. You can see in the following code that AutoCli implements each of the steps mentioned previously:


HRESULT CApp::Invoke(DISPID dispID, WORD wFlags, DISPPARAMS *pdp
, VARIANT *pva, EXCEPINFO *pExInfo, UINT *puErr)
{
HRESULT hr;
LPTSTR pszMsg=NULL;
LPTSTR pszFmt=NULL;
UINT uRet;
UINT uStyle;
TCHAR szSource[80];

if (NULL==m_pIDispatch)
return ResultFromScode(E_POINTER);

hr=m_pIDispatch->Invoke(dispID, IID_NULL, m_lcid, wFlags
, pdp, pva, pExInfo, puErr);

if (DISP_E_EXCEPTION!=GetScode(hr))
return hr;

//If we're given a deferred filling function, fill now.
if (NULL!=pExInfo->pfnDeferredFillIn)
(*pExInfo->pfnDeferredFillIn)(pExInfo);

//Go get the real source name from ProgID.
lstrcpy(szSource, TEXT("Unknown"));

if (NULL!=pExInfo->bstrSource)
{
LONG lRet;

//If this doesn't work, we'll have "Unknown" anyway.
RegQueryValue(HKEY_CLASSES_ROOT, pExInfo->bstrSource
, szSource, &lRet);

SysFreeString(pExInfo->bstrSource);
}

if (NULL!=pExInfo->bstrDescription)
{
pszFmt=(LPTSTR)malloc(CCHSTRINGMAX*sizeof(TCHAR));
pszMsg=(LPTSTR)malloc((CCHSTRINGMAX+lstrlen(szSource)
+lstrlen(pExInfo->bstrDescription))*sizeof(TCHAR));

if (0==pExInfo->wCode)
{
//Formatting for SCODE errors
LoadString(m_hInst, IDS_MESSAGEEXCEPTIONSCODE, pszFmt
, CCHSTRINGMAX);
wsprintf(pszMsg, pszFmt, (long)pExInfo->scode
, (LPTSTR)szSource
, (LPTSTR)pExInfo->bstrDescription);
}
else
{
//Formatting for wCode errors
LoadString(m_hInst, IDS_MESSAGEEXCEPTION, pszFmt
, CCHSTRINGMAX);
wsprintf(pszMsg, pszFmt, (UINT)pExInfo->wCode
, (LPTSTR)szSource
, (LPTSTR)pExInfo->bstrDescription);
}

free(pszFmt);
}
else
{
pszMsg=(LPTSTR)malloc(CCHSTRINGMAX*sizeof(TCHAR));
LoadString(m_hInst, IDS_MESSAGEUNKNOWNEXCEPTION, pszMsg
, CCHSTRINGMAX);
}

uStyle=MB_OK œ MB_ICONEXCLAMATION;

#ifdef MB_HELP
uStyle œ=(NULL!=pExInfo->bstrHelpFile) ? MB_HELP : 0;
#else
uStyle œ=(NULL!=pExInfo->bstrHelpFile) ? MB_OKCANCEL : 0;
#endif

//CApp::Message(string, style) displays message box.
uRet=Message(pszMsg, uStyle);

if (NULL!=pszMsg)
free(pszMsg);

#ifdef MB_HELP
if (IDHELP==uRet)
#else
if (IDCANCEL==uRet)
#endif
{
TCHAR szHelp[512];

if ((TCHAR)0!=m_szHelpDir[0])
{
wsprintf(szHelp, TEXT("%s\\%s"), m_szHelpDir
, pExInfo->bstrHelpFile);
}
else
lstrcpy(szHelp, pExInfo->bstrHelpFile);

WinHelp(NULL, szHelp, HELP_CONTEXT, pExInfo->dwHelpContext);
}

SysFreeString(pExInfo->bstrDescription);
SysFreeString(pExInfo->bstrHelpFile);

return ResultFromScode(DISP_E_EXCEPTION);
}

This code conditionally compiles use of the MB_HELP style for versions of Windows that support the flag (Windows 95 and Windows NT 3.51). If you compile for earlier versions of Windows, cheat and use a Cancel button instead of a Help button because you do not want to complicate this sample with a custom dialog box that would have to dynamically resize itself based on the message length, something MessageBox does automatically.4

The two strings used to format the messages are defined in an AUTOCLI.RC stringtable as "Error %u in %s: %s" (for wCode errors) and "Error %lX in %s: %s" (for scode errors). If there isn't a bstrDescription string, AutoCli uses a generic error message—"An unspecified error occurred in the automation object." This is generic because we have only one object: a more complete controller would probably have another name for the object (instead of "automation object") that it could put in such a message.

Note: If you run AutoCli with Beeper 2, 3, 4, or 5 and then try to run AutoCli with Beeper1, viewing the help file will not work. The reason is that AutoCli uses the presence of HELPDIR in the registry to determine whether it should prepend a path to bstrHelpFile before calling WinHelp. If you register the entries for any of the later Beeper samples, you will create the necessary TypeLib entries, including HELPDIR. If you then register Beeper1 again, those entries will still exist, causing AutoCli to attempt to prepend a path to the full help file path already specified in bstrHelpFile. Swapping registry versions for objects will not happen like this in real practice. If you run into this, you'll have to hack out the TypeLib entries for Beeper by hand.

3 If the controller supports error objects, it can query the automation object for ISupportErrorInfo and call ISupportErrorInfo::InterfaceSupportsErrorInfo to check for available error information in an error object. If any is, the controller can then call GetErrorInfo and various IErrorInfo members to retrieve that exception information. Note that AutoCli does not demonstrate this process.

4 If you've ever hit a general protection fault under Windows NT, you'll see a similar use of the Cancel button, which starts WinDebug to look at the location of the fault. The message in the dialog box says that Cancel starts the debugger, but I've always found that a little confusing. A Debug button would be better. But MessageBox is designed to work under very low memory con-ditions, whereas a custom dialog box is not. Therefore, it is in the best interest of the system to display the message even under the worst possible conditions, and because Windows NT 3.5 doesn't have the MB_HELP style, MB_CANCEL is the next best thing. If you really want to, you can probably get away with using Cancel in the same way, but you should put some sort of message in the message box indicating that clicking Cancel will bring up the appropriate help. It's best, however, to make a real Help button if you can.