The Links Dialog Box and the IOleUILinkContainer Interface

Now we come to what I consider the ugliest part of implementing a container application: the Links dialog box. This dialog is supposed to show all links in the current document. In Patron, I found this difficult to implement because I open only one page at a time, and opening all the pages at once only for this dialog would be a major change to the application architecture. So instead, Patron shows only the links in the current page.

As rich as this dialog is, we're fortunate that it's implemented in the OLE UI Library through OleUIEditLinks, which takes an OLEUIEDITLINKS structure. The only custom field (not common to all the OLE dialogs) in this structure, lpOleUILinkContainer, is a pointer to an interface named IOleUILinkContainer, which is defined in OLEDLG.H for the express purpose of the Links dialog box:


interface IOleUILinkContainer : IUnknown
{
DWORD GetNextLink(DWORD dwLink);
HRESULT SetLinkUpdateOptions(DWORD dwLink, DWORD dwUpdateOpt);
HRESULT GetLinkUpdateOptions(DWORD dwLink
, DWORD *lpdwUpdateOpt);
HRESULT SetLinkSource(DWORD dwLink, LPOLESTR lpszDisplayName
, ULONG lenFileName, ULONG *pchEaten, BOOL fValidateSource);
HRESULT GetLinkSource(DWORD dwLink, LPOLESTR *lplpszDisplayName
, ULONG *lplenFileName, LPOLESTR *lplpszFullLinkType
, LPOLESTR *lplpszShortLinkType, BOOL *lpfSourceAvailable
, BOOL *lpfIsSelected);
HRESULT OpenLinkSource(DWORD dwLink);
HRESULT UpdateLink(DWORD dwLink, BOOL fErrorMessage
, BOOL fErrorAction);
HRESULT CancelLink(DWORD dwLink);
};

This interface is the way the Links dialog box calls back to the container to tell the container to perform actions in response to the end user's actions in the dialog box itself. You can see direct analogs between controls in the dialog (Figure 20-4 on page 954) and the member functions of this interface. GetNextLink is what the dialog uses to enumerate the container's linked objects in order to populate the list box. This is a better mechanism than something like messages or a hook procedure. The table on the following page describes when each function is called.

Function

When Called

GetNextLink, GetUpdateOptions, GetLinkSource

All three of these functions are used to fill the dialog box. The dialog box manages a DWORD for each link in the list box, and GetNextLink is the function called repeatedly to obtain those DWORDs. Typically, this will be some pointer. When the dialog box initially fills the list box, it will, after calling GetNextLink, call GetUpdateOptions and GetLinkSource to obtain additional information to create the list box items.

SetLinkUpdateOptions

Called when the user selects the Automatic or Manual option button.

SetLinkSource

Called when the user makes changes in the Change Source dialog box.

OpenLinkSource

Called when the user chooses Open Source.

UpdateLink

Called when the user chooses Update Now.

CancelLink

Called when the user chooses Break Link.


To invoke the Links dialog box, we'll need an implementation of this interface. Patron's implementation comes from CIOleUILinkContainer, defined in PAGES.H and implemented in IUILINK.CPP. The header comments in the source file itself describe the arguments and necessary behavior of each member. Let me also point out that CIUILinkContainer uses the value IID_IOleUILinkContainer in its QueryInterface function. I could leave this out because the OLE UI Library is the only client of this object, and it never calls QueryInterface. In addition, OLE doesn't define this IID itself—I've defined it for my own uses in INC\BOOKGUID.H with one of my own values. Basically, no standard IID is assigned to this oddball.

As seems typical with my code, a few weird things deserve explanation. First is the extra protected function I've added to CIUILinkContainer. Named GetObjectInterface, the function merely queries for an interface pointer from a tenant identified by the DWORD dwLink. For Patron, every DWORD identifier in this interface is a pointer to a CTenant. Because most of what we use in IOleUILinkContainer is a pointer to IOleLink, the GetObjectInterface function exists to clean up the code everywhere else.

Next, because this is a stand-alone object, it maintains some of its own variables, such as m_pPage, a pointer to the current page from which we can obtain tenant pointers; m_iTenant, which is used to implement GetNextLink; and m_fDirty, which is a public variable in CIOleUILinkContainer and operates in such a way that after invoking the Links dialog box, the code in CPatronDoc can see whether anything happened in the dialog box that would make the document dirty. This is a little inelegant, but it provides an efficient way for the document to know whether any changes occurred.

There's also m_pDelIUILinks, which has to do with the other strange part of this code: a call to CoCreateInstance with CLSID_LinksAssistant found in CIOleUILinkContainer::Init. This CLSID refers to a component (named Links Assistant) that I implemented to help me—and ultimately you—implement the IOleUILinkContainer interface. The source code for this component is found in CHAP20\LNKASSIS and is compiled as LNKASSIS.DLL. You must be sure to register this component to make Patron work properly. (Patron's REG files include the entries as well, for convenience.)


BOOL CIOleUILinkContainer::Init(void)
{
HRESULT hr;

hr=CoCreateInstance(CLSID_LinksAssistant, NULL
, CLSCTX_INPROC_SERVER, IID_IOleUILinkContainer
, (PPVOID)&m_pDelIUILinks);

return SUCCEEDED(hr);
}

Links Assistant essentially provides default implementations of most of the IOleUILinkContainer member functions to which we can delegate, as you'll see in the IUILINK.CPP code. (Yep, we like delegation; it makes life easier.) I created this object because, in my opinion, the developers of the original OLE UI Library were in a hurry and put a greater burden on the container's implementation of IOleUILinkContainer than necessary. Links Assistant contains the container-independent parts of this interface that I think could have been built into the dialog in the first place. Take a look at the source code if you're interested in the grungy details. I won't show any of it here.

That leaves us to look at the interesting parts of IOleUILinkContainer. First, GetNextLink is an iterative function that is called in order to fill the list in the dialog box. The first call passes a 0 in dwLink, meaning "Return the first linked object." What it returns is again a DWORD that all the other functions are passed to identify which object is being manipulated. GetNextLink returns a 0 when there are no more links. Patron is concerned only with keeping an index of the current tenant to return (m_iTenant), and when this function is called, it gets the tenant pointer and checks to see whether it's an embedded or a linked object by means of the function CTenant::TypeGet. (Note that Links Assistant can't implement this function, so it simply returns an error.)


STDMETHODIMP_(DWORD) CIOleUILinkContainer::GetNextLink(DWORD dwLink)
{
PCTenant pTenant;

//If we're told to start sequence, set index to 0.
if (0L==dwLink)
m_iTenant=0;

/*
* On each subsequent call, find next linked object in
* this document and return it. Be sure index is
* incremented for next time this function is called.
*/
for ( ; m_iTenant < m_pPage->m_cTenants; m_iTenant++)
{
if (m_pPage->TenantGet(m_iTenant, &pTenant, FALSE))
{
if (TENANTTYPE_LINKEDOBJECT==pTenant->TypeGet())
{
m_iTenant++;
return (DWORD)pTenant;
}
}
}

//If we hit end of list, this tells dialog to stop.
return 0L;
}

SetUpdateOptions and GetUpdateOptions do little more than call the same named functions in IOleLink. SetUpdateOptions, however, sets the dirty flag if the change succeeds.

SetLinkSource is called when the user changes the source of the link. The implementation of this function is somewhat complex, so most of the meat is down in the Links Assistant object. If the change was successful, we set m_fDirty so that the rest of the application knows. We also must remember whether this change worked for the implementation of GetLinkSource. We do this here with a public flag in CTenant named m_fLinkAvail, which was added to CTenant specifically for this interface. It's part of CTenant because we need to maintain one value for each object, not for the IOleUILinkContainer interface as a whole. With this flag, we start by assuming that the new source is not valid, but if Links Assistant succeeds in changing the source, we can mark it as available. The reason we need to remember is exposed in GetLinkSource, which is called whenever the Links dialog box needs to update the list box entry for an object. GetLinkSource returns a flag indicating whether the link source is available, which is simply CTenant::m_fLinkAvail, and another flag indicating whether the object is currently selected in the container, which the application determines by calling another new function, CTenant::FIsSelected. The rest of the information needed from GetLinkSource is handled by the code in Links Assistant. If you say this object is unavailable, the rightmost column of the list box entry will read Unavail. If you say the object is selected, that entry, as well as any others that you say are selected, will be selected when the dialog box is initially displayed. The selected flag is meaningless anytime thereafter, however.

OpenLinkSource is another way of saying "activate with OLEIVERB_OPEN." So that's what we do. UpdateLink is not quite as simple: Links Assistant calls IOleObject::IsUpToDate, and if it returns S_FALSE, Links Assistant calls IOleObject::Update, which might launch servers to obtain the update. Because the presentation might have changed, we should repaint the container site as well as update the tenant's m_fLinkAvail according to the success of the update. If the argument fErrorMessage is set, we're also responsible for displaying some meaningful error message on failure.

The last function in this interface, CancelLink, is meant to convert a linked object to a static object—that is, it causes the object to lose the ability to be activated and disconnects the object entirely from its link source. Links Assistant disconnects the object by calling IOleLink::SetSourceMoniker(NULL), which forces the linked object to forget about its source completely, although the object still maintains any cached presentations. We want to keep those presentations, however, because a static object still needs something to show for itself.

Removing an object's ability to be activated is handled by yet another new function in Patron, CTenant::ConvertToStatic. I use this function to mark the object as static, which effectively prevents the tenant from activating it. This makes the object appear to be static, although it's still a linked object as far as OLE is concerned:


BOOL CTenant::ConvertToStatic(void)
{
m_tType=TENANTTYPE_STATIC;
return TRUE;
}

The catch in all of this is that we must remember that this object is static when we reload it. Patron handles this situation with a flag in its storage and code in CTenant::ObjectInitialize that calls IOleLink::GetSourceMoniker. Because we set the moniker to NULL, this call will return NULL as well, and we use that condition to reinitialize the object as static.

So that does it for implementing the IOleUILinkContainer interface. Now let's see where we use it.