| bitbucket: C++ MFC ATL WTL Win32 COM ActiveX Samples Tutorials Source Code Controls |
AtlAdviseSinkMap_Ex |
code snippet below Download Sample project (WTL) |
Remove an extra window layer when hosting ActiveX controls - With ATL, you can create a dialog resource or a composite control, that contains another ActiveX control as child window, and provides a very convenient way for the outer class to catch the COM events fired by the child control. This article shows a simple extension to the AtlAdviseSinkMap function to catch the COM events in the host window itself. The sample application shows how to intercept NavigateTo events of an WebBrowser control.
This article explores one deep corner of ATL, and you need some prior knowledge of COM and ATL - or don't mind some fuzzy explanations. The code snippet below is "plug & play", as soon as you understood the problem.
The image to the left shows how ATL handles COM events when you
place an ActiveX control in a dialog:
For the ActiveX control to work as intended, an intermediate "host window" is required. (this is not specific to ATL, but due to the way most ActiveX controls work) This host window implements the client site interfaces expected by the ActiveX, and embeds it smoothly in the window hierarchy. It is identified by IDC_CONTROL1, the ID given to it in the resource editor.
You can now add handlers for the ActiveX' events to the form window, much alike you add notification message handlers for normal windows controls, like an EN_CHANGE handler for edit boxes. In fact, if you use the ATL wizard for this, you won't notice a difference.
Internally, things are a bit more complex. First, an Event Sink Map is added to the class, denoted by the BEGIN_SINK_MAP, SINK_ENTRY_xxx and END_SINK_MAP macros. Further, the form window is (additionally) inherited from an IDispEventImpl<> - specialization. IDispEventImpl implements the required interface for receiving COM events, and redirects these through the event sink map. The event sink map dispatches these events to member functions of the form window. To connect the ActiveX controls with the IDispEventImpl<> specializaitons, AtöAdviseSinkMap is used - more on this below.
IDispEventImpl<> requires quite some template arguments, most of them defaulted
to NULL (refer to the MSDN documentation for detailed info).
The one interesting to us is the first template parameter - UINT nID.
The first role of this template parameter is to distinct between the event sinks of
different controls: When the form window hosts two ActiveX controls, the form window class
is dereived from two specializations of IDispEventImpl<>. Since all other template
parameters might be the same, this one makes sure these base classes are distinguishable.
The second task for the control ID is to distinguish the controls in the sink map, and
send the controls from different controls to different functions. For this, each
SINK_ENTRY uses the same ID to specify to which control they belong.
Until now, the nID is very arbitrary: it must be different for different controls in the form window, and - due to an implementational restriction - they must be positive. But we didn't yet tell the ActiveX control which IDispEventImpl<> - specialization wants to subscribe to the controls events.
This is exactly what AtlAdviseSinkMap does - subscribing the IDispEventImpl<>'s to some controls events. (The correct terms for this are "Advise" and "Unadvise".) To make this process automatic, the nID template parameter must match the
AtlAdviseSinkMap connects tells each ActiveX control which IDispEventImpl<> - specialization wants to subscribe to their events. For this, the nID template parameter must match the dialog control ID specified for the controls host window - IDC_CONTROL1 in the picture above.
This mechanism provides a flexible way to catch COM events from child controls, and the messy implementation details may happily be ignored. The ATL wizard makes this even easier.
However, in one task at hand, I had to handle these events in the host window itself; introducing an additional parent for catching the events "the usual way" was not an option. There are reasons where you want to avoid (or don't even have) the form window; or you want to customize the host window but need to catch events for this. The workaround is quite easy - it would require only a few lines added to the AltAdviseSinkMap implementation. Since the function itself is relatively straightforward (at least, after you digged your head through the other stuff), I decided to make an improved clone instead: AtlAdviseSinkMap_Ex.
inline HRESULT AtlAdviseSinkMap_Ex(T* pT, bool bAdvise)
AtlAdviseSinkMap_Ex has the same signature and usage as AtlAdviseSinkMap: pWnd specifies the window with ther event handlers, and the second parameter specifies whether the event sinks should be advised (connected) or unadvised (disconnected).
However, AtlAdviseSinkMap_Ex allows the Event handler code to be added to the host window directly. To make this work, just specify 0 for the nID template parameter (andin the sink map!) The following snippet shows the code - the modifications are set to bold:
// ==========>>>> ATL RIP
//Helper for advising connections points from a sink map
template <class T>
inline HRESULT AtlAdviseSinkMap_Ex(T* pT, bool bAdvise)
{
ATLASSERT(::IsWindow(pT->m_hWnd));
const _ATL_EVENT_ENTRY<T>* pEntries = T::_GetSinkMap();
if (pEntries == NULL) return S_OK;
HRESULT hr = S_OK;
while (pEntries->piid != NULL) {
_IDispEvent* pDE = (_IDispEvent*)((DWORD)pT+pEntries->nOffset);
bool bNotAdvised = pDE->m_dwEventCookie == 0xFEFEFEFE;
if (bAdvise ^ bNotAdvised) {
pEntries++;
continue;
}
hr = E_FAIL;
// ======> here's the original, and the change
// HWND h = pT->GetDlgItem(pEntries->nControlID);
HWND h = 0;
if (pEntries->nControlID == 0) h = pT->m_hWnd;
else h = pT->GetDlgItem(pEntries->nControlID);
// <====== that's it.
ATLASSERT(h != NULL);
if (h != NULL) {
CComPtr<IUnknown> spUnk;
AtlAxGetControl(h, &spUnk);
ATLASSERT(spUnk != NULL);
if (spUnk != NULL) {
if (bAdvise) {
if (!InlineIsEqualGUID(IID_NULL, *pEntries->piid))
hr = pDE->DispEventAdvise(spUnk, pEntries->piid);
else {
AtlGetObjectSourceInterface(spUnk, &pDE->m_libid, &pDE->m_iid, &pDE->m_wMajorVerNum, &pDE->m_wMinorVerNum);
hr = pDE->DispEventAdvise(spUnk, &pDE->m_iid);
}
}
else {
if (!InlineIsEqualGUID(IID_NULL, *pEntries->piid))
hr = pDE->DispEventUnadvise(spUnk, pEntries->piid);
else
hr = pDE->DispEventUnadvise(spUnk, &pDE->m_iid);
}
ATLASSERT(hr == S_OK);
}
}
if (FAILED(hr)) break;
pEntries++;
}
return hr;
}
// <<<========== ATL RIP
Tip: the wiazrd is still the easiest way to generate the IDispEventImpl<> - delcaraiton and the sink map. So what I usually do is to create a helper ATL project with a dialog, put the ActiveX in quesiton there, let the wizard add the handlers, and then copy the relevant code to my actual project.
Note: The Sample program was created with WTL, you might need to download it from Microsoft if you wnat to compile the sample; CodeProject is a good starting point about WTL.
The sample program uses the WebBrowser control in an WTL SDI application. the "View" class (that hosts the WebBrowser Control) uses above method to intercept the OnBeforeNavigate event, and allows to cancel the navigation.
Download Sample (80K, sample project, documentation, smple executable)
Notes:
26.2.2002: Wrote the entire explanation - the original article only
contained some nbrief reasoning. It's very specialized, and I doubt that this will benefit
someone - it's one of the code snippets I rather see burned & buried. But someday it
might help me remember what I once did.
bitbucket c++ site © Peter Hauptmann. Questions
& Comments to cherea@cherea.de
page updated 29/02/04