The Convert Dialog Box: Conversion and Emulation

Earlier in this chapter, we saw how a server can mark itself as capable of converting data from another server or capable of emulating another server. In OLE Documents, conversion and emulation involve more than simply mapping the CLSIDs because persistent storage is involved. An emulating server must not only be functionally equivalent to those servers it wants to emulate, but it must also be able to both read and write foreign storage formats. A server that can read foreign data and then write it in its own format can convert objects from other servers to its own type, changing the persistent object's CLSID. The way a server handles this is covered in Chapter 18. In that chapter's sample, Cosmo is capable of converting and emulating data from its OLE 1 version server as well as from the Polyline component that we'll work with in Chapter 19.

To facilitate conversion and emulation, a container is responsible for providing the user interface necessary to show the user what conversion and emulation support exists for a given object type. This allows a user to create a compound document with one type of object from one particular server and give it to another user who does not have that server installed. When the second user attempts to activate the object, no server will be available. In that case, the container can display the Convert dialog box, shown in Figure 17-9 on the following page, through which the user can either permanently convert the object to a different type or indicate which server to use to emulate that object type not only now but for all such encounters in the future.

Figure 17-9.

The Convert dialog box invoked for a Polyline object (Chapter 19) showing that Cosmo (Chapter 18) can convert it.

For a container, conversion means that we have to unload the object, mark it with a new CLSID, reload it using the new server, and have that server munge the storage into the new object type. This is the Convert To case. Emulation requires making the necessary TreatAs entries in the registry and also that we unload the object completely and then reload it to load any handlers needed by the new type. This is the Activate As case. In addition, the Convert dialog box can be used to simply switch between DVASPECT_ICON and DVASPECT_CONTENT views of an object.

In all of these cases, we must first invoke the Convert dialog with OleUIConvert. This happens in response to two events: either the user selects the Convert menu item from the object's verb menu or some other attempt to activate the object failed and we now want to give the user the chance to convert or emulate. In Patron, either case winds up in the function CPage::ConvertObject, in which the fNoServer argument, when TRUE, specifies that we're coming here because of a failed activation. In all honesty, Patron doesn't use this flag at all, but it demonstrates what you would do in that case:


BOOL CPage::ConvertObject(HWND hWndFrame, BOOL fNoServer)
{
HRESULT hr;
OLEUICONVERT ct;
TENANTTYPE tType;
FORMATETC fe;
TENANTINFO ti;
UINT uRet;
HCURSOR hCur;
BOOL fActivate=fNoServer;
RECTL rcl;

[Validation; exit immediately for static objects.]

//Get object information we might want.
m_pTenantCur->GetInfo(&ti);

//Fill the structure.
memset(&ct, 0, sizeof(ct));
ct.cbStruct=sizeof(OLEUICONVERT);
ct.hWndOwner=hWndFrame;
ct.fIsLinkedObject=FALSE;
ct.dvAspect=ti.fe.dwAspect;

m_pTenantCur->ObjectClassFormatAndIcon(&ct.clsid, &ct.wFormat
, &ct.lpszUserType, &ct.hMetaPict, &ct.lpszDefLabel);

uRet=OleUIConvert(&ct);

[Code to handle the three cases goes here.]

CoTaskMemFree((void*)ct.lpszUserType);
INOLE_MetafilePictIconFree(ct.hMetaPict);
return TRUE;
}

The function CTenant::ObjectClassFormatAndIcon retrieves information about the object needed in the dialog: the CLSID, the format of the data (a clipboard format, whatever would appear in the registry under a server's Conversion entries), a string with the object's user type, the current iconic presentation (if any), and a default label for the icon. The tenant uses the helper function INOLE_GetUserTypeOfClass and the OLE API function OleGetIconOfClass to retrieve this information. You can see the details in the source code.

Now the Convert dialog box, inside OleUIConvert, will display any other servers that have registered conversion formats matching the values in the wFormat field of OLEUICONVERT. This may, in fact, be an empty list, and if you want to prevent the user from possibly seeing an empty list, you can call the function OleUICanConvertOrActivateAs, using the result to disable the Convert menu in the first place. I don't use this function in Patron because we change iconic aspects through the Convert dialog box.

Anyway, in the dialog the user can either change the icon, select a new format to convert to, or choose to emulate the existing object with a new server. These three cases are differentiated by the dwFlags field in the OLEUICONVERT structure on return. We'll see what we do in response to each, but first remember that in all of these cases, the flag m_fRepaintEnabled in CTenant is used to suppress repaints through this whole process because we might be unloading, reloading, and running servers. Turning off repaints is a great way to cut down on flicker.

Switch Display Aspects

The first thing we do when we get back from the Convert dialog box in CPage::ConvertObject is execute the following code:


if ((DVASPECT_ICON==ct.dvAspect && ct.fObjectsIconChanged)
œœ ct.dvAspect!=ti.fe.dwAspect)
{
HGLOBAL hMem=NULL;

//Only pass non-NULL handle for icon aspects.
if (DVASPECT_ICON==ct.dvAspect)
hMem=ct.hMetaPict;

m_pPG->m_fDirty=m_pTenantCur->SwitchOrUpdateAspect(hMem
, FALSE);
}

This code handles two cases: either we switched display aspects or we simply changed the icon, leaving the aspect the same. (The latter is indicated with the Convert dialog box's fObjectsIconChanged flag.) CTenant::SwitchOrUpdateAspect handles both cases. In the former, it switches the contents of the cache, getting an update from the object if necessary. In the second case, it merely stuffs the new icon in the cache using the helper function INOLE_SetIconInCache. All of this is again basic cache manipulation.

When we do change the aspect, the tenant reinitializes its size using IViewObject2::GetExtent or IOleObject::GetExtent. When we switch to an icon, we want the tenant to show up with a reasonable size for the icon. Switching back to a content aspect should revert to a larger object size.

Handle the Convert To Case

If the end user selects Convert To in the dialog box and then chooses OK, OleUIConvert will return with CF_SELECTCONVERTTO in the dwFlags field. A container must then execute four steps:

Unload the object, taking it back to the passive state.

Modify the class and format saved in the object's persistent storage by calling WriteClassStg, WriteFmtUserTypeStg, and SetConvertStg.

Reload the object with OleLoad and force an update. (This means set the document's dirty flag and force a repaint.)

If the Convert dialog box was invoked as a result of a failed activation, activate the object now.

These steps can be seen in the following code, taken from Patron's CPage::ConvertObject:


BOOL            fActivate=fNoServer;
RECTL rcl;
§
if ((CF_SELECTCONVERTTO & ct.dwFlags)
&& ct.clsid!=ct.clsidNew)
{
LPSTORAGE pIStorage;

m_pTenantCur->StorageGet(&pIStorage);
m_pTenantCur->Close(TRUE);

hr=INOLE_DoConvert(pIStorage, ct.clsidNew);
pIStorage->Commit(STGC_DEFAULT);
pIStorage->Release();

if (SUCCEEDED(hr))
{
LPUNKNOWN pObj;
LPOLEOBJECT pIOleObject;

//Reload and update.
m_pTenantCur->Load(m_pIStorage, &ti);

m_pTenantCur->ObjectGet(&pObj);
pObj->QueryInterface(IID_IOleObject
, (PPVOID)&pIOleObject);
pIOleObject->Update();
pIOleObject->Release();
pObj->Release();
}

m_pPG->m_fDirty=TRUE;
}

m_pTenantCur->Repaint();

if (fActivate)
m_pTenantCur->Activate(OLEIVERB_SHOW);

It's necessary to close and reload the object because the new class we're converting to might have a specific object handler, and we have to be sure that handler is now loaded for this object. The process of taking the object to the passive state and then reloading it does the trick.

The helper function INOLE_DoConvert handles the calls to WriteClassStg, WriteFmtUserTypeStg, and SetConvertStg for us. The latter function, which we haven't seen before, specifically marks the IStorage for this object as being converted to a new CLSID, which a server will use to detect that conversion is happening when it loads the object data in its IPersistStorage::Load. We'll see this in Chapter 18.

After we have marked the object's storage appropriately, we can reload the object (CTenant::Load) and update it with IOleObject::Update, which will send IAdviseSink::OnViewChange notifications as necessary. The final step is that if we invoked the Convert dialog because an activation failed (REG_E_CLASSNOTREG or CO_E_APPNOTFOUND from IOleObject::DoVerb), we should not activate the newly converted object because that's what the user wanted to do originally.

Handle the Activate As Case

The Convert dialog box might also return the CF_SELECTACTIVATEAS in dwFlags, in which case, we need to perform the following four steps:

Add the TreatAs entry in the registry by calling call CoTreatAsClass.

Unload all objects of the old CLSID that you have loaded. You can do this before step 1 if necessary.

Reload all the unloaded objects as necessary.

Set your document's dirty flag to TRUE, activate the current object, and repaint.

These steps also are shown in the code from Patron's CPage::ConvertObject:


BOOL            fActivate=fNoServer;

if (CF_SELECTACTIVATEAS & ct.dwFlags)
{
hr=CoTreatAsClass(ct.clsid, ct.clsidNew);

if (SUCCEEDED(hr))
{
LPTENANT pTenant;
UINT i;

for (i=0; i < m_cTenants; i++)
{
if (TenantGet(i, &pTenant, FALSE))
{
pTenant->GetInfo(&ti);
pTenant->Close(FALSE);
pTenant->Load(m_pIStorage, &ti);
}
}

fActivate=TRUE;
}
}
§
m_pTenantCur->Repaint();

if (fActivate)
m_pTenantCur->Activate(OLEIVERB_SHOW);

CoTreatAsClass, as we saw in Chapter 5, establishes a permanent emulation from the old CLSID to the new CLSID. After that, we unload and reload every tenant in the current page (all that are loaded) to ensure that they are now using the right class. We unload all the tenants because we cannot be certain that one object isn't using an object of the old class that is now being emulated. So we unload everything. In addition, we always activate the object on which the Convert dialog box was originally invoked. The user saw Activate As in the dialog and expects that activation will now take place. It's a good idea to meet that expectation.