Implementing a Data Object: DDataObj and EDataObj

This section explores the concepts of Uniform Data Transfer through code samples, looking at data objects (single objects in a component server) based on both a DLL and an EXE. (These samples are found in CHAP10\DDATAOBJ and CHAP10\EDATAOBJ.) The next section, "Implementing a Consumer: DataUser," looks at a sample (CHAP10\DATAUSER) that works in conjunction with the data objects here.

Both DDataObj and EDataObj servers share a common object implementation in the C++ class CDataObject, which has only the IDataObject interface. Its implementation is found in the files DATAOBJ.CPP (the object's core), IDATAOBJ.CPP (the IDataObject interface), and RENDER.CPP (specific code for rendering individual formats). This data object, which is independent of the DLL or EXE server structure (like the Koala objects of Chapter 5), uses the CEnumFormatEtc implementation mentioned earlier in this chapter for its handling of IDataObject::EnumFormatEtc and also uses OLE's data advise holder service.

The data object itself supports full-content renderings (DVASPECT_CONTENT) for the formats of CF_TEXT, CF_BITMAP, and CF_METAFILEPICT. The text is a certain number of characters, the bitmap is an image (see CHAP10\RES for the BMP files), and each metafile is a series of rectangles filled with various shades of blue. Both DDataObj and EDataObj actually support three CLSIDs corresponding to three sizes of data:

CLSID

Text
(Characters)

Bitmap
(x * y)

Metafile
(FillRect Records)

CLSID_DataSmall

64

16 * 16

16

CLSID_DataMedium

1024

64 * 64

128

CLSID_DataLarge

16,384

256 * 256

1024


All three classes are handled with the same class factory, named CDataObjectClassFactory, which tells each data object it creates about which data set to render for the consumer. The servers described here are good examples of supporting multiple CLSIDs—DDataObj creates a different class factory instance for each CLSID, whereas EDataObj creates all three of them on startup, registering them separately.

The CDataObject implementation (DATAOBJ.CPP) has a number of features worth mentioning. First, only the get direction is supported. SetData always returns E_NOTIMPL, as does GetDataHere, because CDataObject doesn't support any variable-length formats:


STDMETHODIMP CImpIDataObject::GetData(LPFORMATETC pFE
, LPSTGMEDIUM pSTM)
{
UINT cf=pFE->cfFormat;

//Check aspects we support.
if (!(DVASPECT_CONTENT & pFE->dwAspect))
return ResultFromScode(DATA_E_FORMATETC);

switch (cf)
{
case CF_METAFILEPICT:
if (!(TYMED_MFPICT & pFE->tymed))
break;

return m_pObj->RenderMetafilePict(pSTM);

case CF_BITMAP:
if (!(TYMED_GDI & pFE->tymed))
break;

return m_pObj->RenderBitmap(pSTM);

case CF_TEXT:
if (!(TYMED_HGLOBAL & pFE->tymed))
break;

return m_pObj->RenderText(pSTM);

default:
break;
}

return ResultFromScode(DATA_E_FORMATETC);
}

STDMETHODIMP CImpIDataObject::GetDataHere(LPFORMATETC pFE
, LPSTGMEDIUM pSTM)
{
return ResultFromScode(E_NOTIMPL);
}


STDMETHODIMP CImpIDataObject::SetData(LPFORMATETC pFE
, LPSTGMEDIUM pSTM, BOOL fRelease)
{
return ResultFromScode(DATA_E_FORMATETC);
}

The CDataObject member functions RenderText, RenderBitmap, and RenderMetafilePict are found in RENDER.CPP. The first function allocates global memory and writes a stream of characters into it, the second loads a bitmap from the server's resources, and the third creates a metafile device context into which it makes a number of FillRect calls. Again, the number of characters in the text, the dimensions of the bitmap, and the number of FillRect calls made into the metafile depend on the size of the data object as determined by the CLSID that was used to create it in the first place (a DOSIZE_* value as defined in DATAOBJ.H). The large metafile, for example, looks much like the shaded blue background you see in Microsoft's standard setup program; each rectangle is only a few pixels high, and each rectangle's color differs only slightly from the previous one. The small metafile, on the other hand, has only four rectangles of very different shades.

It is very important for GetData to fill the entire STGMEDIUM structure. It's easy to forget to fill the tymed field and to set the pUnkForRelease field to NULL when necessary. If you forget to set tymed, OLE will not marshal the data rendering between processes correctly—it has to know the exact nature of that data to copy it correctly. If you place an invalid pointer in pUnkForRelease, OLE can cause a fault inside ReleaseStgMedium, especially with an in-process data object.

By definition, GetDataHere tries to render data into a caller-allocated storage medium but does not attempt to reallocate space itself. Generally the only storage mediums you need to support in this function are TYMED_ISTORAGE, TYMED_ISTREAM, and TYMED_FILE. If you don't support these mediums, you don't need to support this function; if you do, return STG_E_MEDIUMFULL if writing to such storage fails.

The implementation of QueryGetData in the samples responds only to CF_TEXT, CF_BITMAP, and CF_METAFILEPICT, as you might expect, and GetCanonicalFormatEtc always returns DATA_S_SAMEFORMATETC, indicating that all renderings are identical for each format:


STDMETHODIMP CImpIDataObject::QueryGetData(LPFORMATETC pFE)
{
UINT cf=pFE->cfFormat;
BOOL fRet=FALSE;

//Check aspects we support.
if (!(DVASPECT_CONTENT & pFE->dwAspect))
return ResultFromScode(DATA_E_FORMATETC);

switch (cf)
{
case CF_METAFILEPICT:
fRet=(BOOL)(pFE->tymed & TYMED_MFPICT);
break;

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

case CF_TEXT:
fRet=(BOOL)(pFE->tymed & TYMED_HGLOBAL);
break;

default:
fRet=FALSE;
break;
}

return fRet ? NOERROR : ResultFromScode(S_FALSE);
}

STDMETHODIMP CImpIDataObject::GetCanonicalFormatEtc
(LPFORMATETC pFEIn, LPFORMATETC pFEOut)
{
if (NULL==pFEOut)
return ResultFromScode(E_INVALIDARG);

pFEOut->ptd=NULL;
return ResultFromScode(DATA_S_SAMEFORMATETC);
}

For the purposes of EnumFormatEtc, CDataObject keeps its supported formats ordered in the array m_rgfeGet, which is initialized in the object's constructor. This array is passed to the CEnumFormatEtc constructor from within EnumFormatEtc. (We could, of course, achieve the same end with registry entries.)


CDataObject::CDataObject(LPUNKNOWN pUnkOuter
, PFNDESTROYED pfnDestroy, UINT iSize)
{
UINT i;

[Other initialization omitted]
m_cfeGet=CFORMATETCGET;

SETDefFormatEtc(m_rgfeGet[0], CF_METAFILEPICT, TYMED_MFPICT);
SETDefFormatEtc(m_rgfeGet[1], CF_BITMAP, TYMED_GDI);
SETDefFormatEtc(m_rgfeGet[2], CF_TEXT, TYMED_HGLOBAL);

[Other code omitted]
}

STDMETHODIMP CImpIDataObject::EnumFormatEtc(DWORD dwDir
, LPENUMFORMATETC *ppEnum)
{
switch (dwDir)
{
case DATADIR_GET:
*ppEnum=new CEnumFormatEtc(m_pObj->m_cfeGet
, m_pObj->m_rgfeGet);
break;

case DATADIR_SET:
*ppEnum=NULL;
break;

default:
*ppEnum=NULL;
break;
}

if (NULL==*ppEnum)
return ResultFromScode(E_FAIL);
else
(*ppEnum)->AddRef();

return NOERROR;
}

You can see how this implementation supports only the get direction through EnumFormatEtc, failing requests for the set direction. In addition, it uses the CEnumFormatEtc class found in IENUMFE.CPP for the enumerator implementation.

The object then uses CreateDataAdviseHolder and the IDataAdviseHolder interface to implement the members of IDataObject related to notification, creating the advise holder once inside DAdvise and releasing it in the object's destructor:


STDMETHODIMP CImpIDataObject::DAdvise(LPFORMATETC pFE, DWORD dwFlags
, LPADVISESINK pIAdviseSink, LPDWORD pdwConn)
{
HRESULT hr;

if (NULL==m_pObj->m_pIDataAdviseHolder)
{
hr=CreateDataAdviseHolder(&m_pObj->m_pIDataAdviseHolder);

if (FAILED(hr))
return ResultFromScode(E_OUTOFMEMORY);
}

hr=m_pObj->m_pIDataAdviseHolder->Advise((LPDATAOBJECT)this, pFE
, dwFlags, pIAdviseSink, pdwConn);

return hr;
}

STDMETHODIMP CImpIDataObject::DUnadvise(DWORD dwConn)
{
HRESULT hr;

if (NULL==m_pObj->m_pIDataAdviseHolder)
return ResultFromScode(E_FAIL);

hr=m_pObj->m_pIDataAdviseHolder->Unadvise(dwConn);

return hr;
}

STDMETHODIMP CImpIDataObject::EnumDAdvise(LPENUMSTATDATA *ppEnum)
{
HRESULT hr;

if (NULL==m_pObj->m_pIDataAdviseHolder)
return ResultFromScode(E_FAIL);

hr=m_pObj->m_pIDataAdviseHolder->EnumAdvise(ppEnum);
return hr;
}

CDataObject::~CDataObject(void)
{
[Other cleanup omitted]

ReleaseInterface(m_pIDataAdviseHolder);

[Other code omitted]
}

Finally, each data object creates a small visible window, as shown in Figure 10-2, for the purposes of demonstrating data change notifications and comparable exchange rates for in-process and local data objects. Each window has only a caption bar (specifying the server as a DLL or an EXE) and an Iterations menu, but no system menu. (The window is destroyed only when the object is destroyed.) From the Iterations menu, you can tell the data object to fire off a small number to a large number of IAdviseSink::OnData-Change calls to any connected consumer. The data object counts the time it takes to make all of the calls and reports that time along with an average time per call in a message box when the iterations are complete.

Figure 10-2.

A window created by an instance of CDataObject for the small data set.

All the notifications are sent through IDataAdviseHolder::SendOnDataChange inside AdviseWndProc in DATAOBJ.CPP:


LRESULT APIENTRY AdvisorWndProc(HWND hWnd, UINT iMsg
, WPARAM wParam, LPARAM lParam)
{
PCDataObject pDO;
DWORD i;
DWORD iAdvise;
DWORD dwTime;
DWORD dwAvg;
TCHAR szTime[128];
TCHAR szTitle[80];
HCURSOR hCur, hCurT;

pDO=(PCDataObject)GetWindowLong(hWnd, 0);

switch (iMsg)
{
case WM_NCCREATE:
pDO=(PCDataObject)(((LPCREATESTRUCT)lParam)
->lpCreateParams);
SetWindowLong(hWnd, 0, (LONG)pDO);
return (DefWindowProc(hWnd, iMsg, wParam, lParam));

case WM_CLOSE:
//Forbid task manager to close us.
return 0L;

case WM_COMMAND:
if (NULL==pDO->m_pIDataAdviseHolder)
break;

//Send IAdviseSink::OnDataChange many times.
i=(DWORD)(LOWORD(wParam)-IDM_ADVISEITERATIONSMIN+1);
iAdvise=(i*i)*16;

hCur=LoadCursor(NULL, MAKEINTRESOURCE(IDC_WAIT));
hCurT=SetCursor(hCur);
ShowCursor(TRUE);

dwTime=GetTickCount();

i=0;
while (TRUE)
{
[A PeekMeesage loop in the 16-bit EXE server only]
pDO->m_pIDataAdviseHolder->SendOnDataChange
(pDO->m_pImpIDataObject, 0, ADVF_NODATA);

if (++i >= iAdvise)
break;
}

dwTime=GetTickCount()-dwTime;
dwAvg=dwTime/iAdvise;

SetCursor(hCurT);
ShowCursor(FALSE);

wsprintf(szTime
, TEXT("Total\t=%lu ms\n\rAverage\t=%lu ms")
, dwTime, dwAvg);

GetWindowText(hWnd, szTitle, sizeof(szTitle));
MessageBox(hWnd, szTime, szTitle, MB_OK);
break;

default:
return (DefWindowProc(hWnd, iMsg, wParam, lParam));
}

return 0L;
}

These iterations give you the chance to test the relative performance of data change notifications between DLL and EXE servers. As you'd expect, the DLL rates are much faster because nothing exists between the object and the consumer with the in-process case. In this sample, CDataObject accepts only ADVF_NODATA advises, so we can determine the flat-out notification time independently and not have to include the time it actually takes to render the data, which would skew the results.