Inside the Advise Sink

The GetData On Change and Paint On Change menu items allow you to control how IAdviseSink::OnDataChange behaves. Two flags in CApp, m_fGetData and m_fRepaint, hold the current state of your menu selections. DataUser disables both of these menu items and sets these flags to FALSE unless it's using the in-process server DDataObj as the source of the data objects. Reasons for this will become clear shortly.

As you might expect, only the implementation of OnDataChange is important in the advise sink (besides the IUnknown members) because the other notifications are not relevant to data objects themselves. The bulk of code in the sink is contained in this single function:


STDMETHODIMP_(void) CAdviseSink::OnDataChange(LPFORMATETC pFE
, LPSTGMEDIUM pSTM)
{
BOOL fUsable=TRUE;
UINT cf;

STGMEDIUM stm;

if (!m_pApp->m_fGetData && !m_pApp->m_fEXE)
return;

//See whether we're interested in format and aspect that changed.
cf=pFE->cfFormat;

if ((CF_TEXT!=cf && CF_BITMAP!=cf && CF_METAFILEPICT!=cf)
œœ !(DVASPECT_CONTENT & pFE->dwAspect))
return;

//Check medium if we got data.
switch (cf)
{
case CF_TEXT:
fUsable=(BOOL)(TYMED_HGLOBAL & pFE->tymed);
break;

case CF_BITMAP:
fUsable=(BOOL)(TYMED_GDI & pFE->tymed);
break;

case CF_METAFILEPICT:
fUsable=(BOOL)(TYMED_MFPICT & pFE->tymed);
break;

default:
break;
}

if (!fUsable)
return;

if (NULL==m_pApp->m_pIDataObject)
return;

if (m_pApp->m_fEXE)
{
ReleaseStgMedium(&(m_pApp->m_stm));
m_pApp->m_cf=cf;
m_pApp->m_stm.tymed=TYMED_NULL;

InvalidateRect(m_pApp->m_hWnd, NULL, TRUE);
return;
}

if (FAILED(m_pApp->m_pIDataObject->GetData(pFE, &stm)))
return;

//Get rid of old data and update.
ReleaseStgMedium(&(m_pApp->m_stm));

m_pApp->m_cf=cf;
m_pApp->m_stm=stm;

InvalidateRect(m_pApp->m_hWnd, NULL, TRUE);

if (m_pApp->m_fRepaint)
UpdateWindow(m_pApp->m_hWnd);

return;
}

On entry, this function immediately checks whether the m_fGetData flag is FALSE (and whether we're specifically using DDataObj so that CApp::m_fEXE is FALSE), and if so, it returns immediately, doing nothing else. If you remove the additional check on m_fEXE in the first line, you can run basic performance tests between DDataObj and EDataObj as far as raw notification rates are concerned. To run such a test, start DataUser, select Advise for any format (it doesn't matter which format), switch to the Small DLL Advisor #1 window, and select 572 from the Iterations menu. This causes that object to send 572 OnDataChange calls, but because you probably have a fast machine (486/66), you'll end up with a short elapsed time—perhaps 90 ms—with an average time per call of less than 1 ms (0.16 ms), or 6350 calls per second. Now select DataUser's Use EXE Object menu item, select Advise for any format, switch to the Small EXE Advisor #1 window, and select 572 from the Iterations menu again. After a considerably longer time lapse, you'll get a message box reporting the total time, on the relative order of 1532 ms, for an average of 2.7 ms per call, or 370 calls per second. You can see that working with an in-process data object is somewhat faster than working with a local one, simply because there is no marshaling overhead involved. This overhead, in fact, is where most of the extra time is spent: even with the extra m_fEXE condition intact (and the calls to ReleaseStgMedium and InvalidateRect), the total time for a local object to send 572 notifications was 1603 ms on my machine, hardly different from before. Still, 370 notifications per second is a fast rate, good enough for most notification purposes.

Anyway, if OnDataChange proceeds past this first check, it then ensures that the data object sent a notification for the format, aspect, and storage medium you're interested in watching. If there are any differences, it ignores the call altogether. Otherwise, what it does next depends on the server in question. If you're using EDataObj, OnDataChange frees whatever data it currently holds (by calling ReleaseStgMedium) and saves the format that changed inside CApp::m_stm.cf but marks that storage medium as TYMED_NULL. After this, it invalidates but does not update the main window. OnDataChange is telling the main application that it should repaint itself but that it has to call IDataObject::GetData when repainting occurs because the data is not yet available. TYMED_NULL indicates the absence of data, and the call to InvalidateRect basically posts a WM_PAINT to the main window. Therefore, when painting next occurs, DataUser will no longer be inside IAdviseSink::OnDataChange. It detects that it doesn't have a rendering, so it must ask the data object for one before painting, which all occurs in CApp::Paint:


void CApp::Paint(void)
{
[Other code omitted]

if (m_fEXE)
{
if (TYMED_NULL==m_stm.tymed && 0!=m_cf)
{
SETDefFormatEtc(fe, m_cf, TYMED_HGLOBAL
œ TYMED_MFPICT œ TYMED_GDI);

if (NULL!=m_pIDataObject)
m_pIDataObject->GetData(&fe, &m_stm);
}
}

[Other code omitted]
}

You can see how this works when you run iterations from one of the EXE object windows. When the notifications have all been sent, DataUser draws the data in whatever format you selected. With a large number of iterations, you might also see a repaint occur during the iterations themselves because DataUser gets a chance to process some of its own messages, one of which could be WM_PAINT.

If DataUser is talking to an in-process data object, it can turn around and request a new rendering right away by calling IDataObject::GetData. Because there is no marshaling with an in-process object, there is no chance of such a call being rejected. If it gets the new data and then releases the old rendering that's held in CApp:m_stm, it replaces that rendering with the new one and invalidates the window to post a WM_PAINT message. Furthermore, if you've selected the Paint On Change menu item, DataUser calls UpdateWindow to force an immediate repaint. This gives you an opportunity to evaluate performance differences between a delayed repaint scheme and a synchronous one. Of course, repainting every time makes the whole process take longer because the data object will be waiting for all of this to occur before the call to OnDataChange returns.

It is a worthwhile exercise to modify these samples in order to send the data along with each notification and then to compare the relative performance in each case. For all cases except when DataUser asks the in-process object for a rendering inside OnDataChange, you will see a noticeable performance drop. In the cases outlined earlier, very few actual calls were sent into IDataObject::GetData. Sending the data with every notification means rendering the data each time and also freeing the data each time. This can get very expensive where large bitmaps or metafiles are concerned. The techniques demonstrated in DataUser and the two data objects represent more efficient implementations.