A Simple Client-Side Message Filter: ObjectUser2

A client application will want to install a message filter to determine when a server has rejected a call or is otherwise not responding or to process Windows messages that come into the application message queue while a call is in progress. As described earlier, OLE's default message filter will cancel any rejected or delayed call and discard any input message. If you want to change this behavior, you need to install a message filter.

To demonstrate the client side of message filtering, the ObjectUser2 program registers a message filter and provides a menu item to call IPersist::GetClassID on a Koala object obtained from EKoala3. IPersist is used here because it's the simplest interface for which OLE provides standard marshaling support (which means that we don't have to introduce some other complex interface into this specific sample). It is also a call that the proxy will forward to the object—we can't use AddRef or Release in this demonstration because OLE's standard proxy doesn't forward anything except the last Release. So IPersist::GetClassID fits the bill nicely.

ObjectUser2 uses a C++ class, CMessageFilter, which is similar to that in EKoala3 except that the version here fully implements RetryRejectedCall, leaves MessagePending the same as the default filter (returns PENDINGMSG_WAITDEFPROCESS) and returns E_NOTIMPL from HandleInComingCall. The application initialization code instantiates and registers the filter in CApp::Init and removes and releases it in CApp::~CApp, exactly as shown earlier with EKoala3. In other words, the registration process is identical for both clients and servers.

Inside ObjectUser2's RetryRejectedCall, we immediately cancel a rejected call (which returns RPC_E_CALL_REJECTED from the call itself). If the call is merely delayed, we print a message with the time elapsed since the call was made. If 5 seconds elapse, we display the busy dialog box:


STDMETHODIMP_(DWORD) CMessageFilter::RetryRejectedCall
(HTASK htaskCallee, DWORD dwTickCount, DWORD dwRejectType)
{
UINT uRet;
TCHAR szMsg[256];

if (SERVERCALL_REJECTED==dwRejectType)
return (DWORD)-1;

wsprintf(szMsg, TEXT("RetryRejectedCall waiting %lu")
, dwTickCount);
m_pApp->Message(szMsg);

if (dwTickCount < 5000)
return 200;

m_pApp->Message
(TEXT("CMessageFilter::RetryRejectedCall showing busy dialog box"));

uRet=DisplayBusyDialog(htaskCallee, 0L);

switch (uRet)
{
case OLEUI_CANCEL:
return (DWORD)-1;

case OLEUI_BZ_SWITCHTOSELECTED:
/*
* This case won't happen without BZ_NOTRESPONDINGDIALOG,
* but we would wait maybe 10 seconds if it did happen.
*/
return 10000;

case OLEUI_BZ_RETRYSELECTED:
m_pApp->Message(TEXT("Waiting another second"));
return 1000;

default:
break;
}

return 0;
}

UINT CMessageFilter::DisplayBusyDialog(HTASK hTask, DWORD dwFlags)
{
OLEUIBUSY bz;

//Clear out everything we don't use.
memset(&bz, 0, sizeof(bz));

bz.cbStruct=sizeof(OLEUIBUSY);
bz.dwFlags=dwFlags;
bz.hWndOwner=m_pApp->m_hWnd;
bz.hTask=hTask;
bz.lphWndDialog=NULL;

return OleUIBusy(&bz);
}

The OLEUI_CANCEL and OLEUI_BZ_* flags indicate the button the user chose in the dialog box. If the user clicks on the Cancel button in the dialog box, RetryRejectedCall will cancel the call, generating an RPC_E_CALL_REJECTED result. If the user clicks on Retry, we wait for another second by returning 1000 (milliseconds). If the call hasn't gone through by the time that second has elapsed, we come back into RetryRejectedCall and display the dialog box again as long as the user keeps clicking on Retry.

If the user chooses Switch To, the busy dialog box sets the focus to the server if possible (using the server's task handle to get at the window). If the dialog box can't find a window, it launches the Windows task manager. This tells the user to fix whatever the problem is in the server before switching back to the client. When the user does switch back, the busy dialog box automatically closes as if he or she had clicked on the Retry button, in which case our code will wait another second.

One of the little differences between the busy dialog box and the "not responding" dialog box is that choosing Switch To in the latter will, after switching to the server (or task manager), close the dialog box immediately and return OLEUI_BZ_SWITCHTOSELECTED. The client can choose to continue waiting (as the code on the preceding page would do), cancel the call, or wait for a while and then cancel the call if the server still doesn't respond (without invoking the dialog box again).

With ObjectUser2 and EKoala3, you can now fully explore how message filtering works. Run ObjectUser2 and choose Create. After going through all of EKoala3's IExternalConnection messages, choose the IPersist::GetClassID menu item in ObjectUser2. This call will succeed. Now choose Block in EKoala3 and try the call again. This time it fails with 0x80010001, or RPC_E_CALL_REJECTED. Now turn off Block, turn on Delay, and try the call again. This time you'll see ObjectUser2 show the elapsed time while it's waiting through RetryRejectedCall. While this is going on, turn off Delay in EKoala3, and you'll see the call completed with NOERROR, showing how a client will wait for a while and, if the server responds in the right amount of time, proceed without bothering the end user.

Now turn Delay back on and make the call again, but this time let ObjectUser2's counter go past 5000; now the busy dialog box appears. If you click on Cancel, the call will fail with RPC_E_CALL_REJECTED. If you click on Retry, you'll see a message in ObjectUser2, after which the busy dialog box appears again. You can keep choosing Retry as long as you want, to no avail. Now choose Switch To, which will place you in EKoala3. Turn Delay off and switch back to ObjectUser2, where you'll see the busy dialog box disappear. After waiting another second, you'll see the call succeed. You can see the same thing happen if, after you click on Retry, you quickly switch to EKoala3 and turn off Delay. (Or turn Block on and watch the call fail immediately.)

In all, these two samples let you take a look at the variations of rejected and delayed calls. If you watch ObjectUser2's IMessageFilter::MessagePending function as well, you'll see that it gets called during any waiting period if you press a key or do anything with the mouse in ObjectUser2's window, even moving the mouse over it. The preceding code doesn't generate any kind of message in such a case because it would overwrite those messages shown in RetryRejectedCall. But you can prove that calls are being made by inserting the following line into the code:


m_pApp->Message(TEXT("MessagePending called."));

ObjectUser2 is itself a pretty lame application. It doesn't really care about input messages from the mouse and keyboard, so default processing from MessagePending is fine. But what if you want to preserve messages?