An ATL control for hosting and customization of multiple instances of WebBrowser control for VB






4.77/5 (21 votes)
May 12, 2005
13 min read

1088228

11102
An article on WebBrowser hosting and customization.
- Download source and binaries - 323 Kb
- Download VB demo project source and binaries - 68.2 Kb
- Download MFC demo project source and binaries - 59.5 Kb
Index
- Introduction
- Background
Implementation challenges:
- Brief overview of classes
- Setting up the control
- About demo applications
- Control information
- Related documents
- History
Introduction
The goal of this project is to replace web browser wrapper control with one that allows developers to create and use a web browser control having total control over GUI, context menus, accelerator keys, downloads, security, ... without using any sub classing, registry or hacks. The result is an ATL control:
- Which is as easy to use as any web browser wrapper control. Register vbMHWB.dll and use it as any other ActiveX control.
- Allows viewing of all request headers (HTML, images, CSS, ...) with the option of adding additional headers (HTTP + HTTPS).
- Allows viewing of all response headers (HTTP + HTTPS).
- Allows GUI customization using
DOC_HOST_UI_FLAGS
per web browser control instance or globally. NO3DBORDER, ... - Allows behavior customization using
DOC_DOWNLOAD_CONTROL_FLAGS
per web browser control instance or globally.DLIMAGES
,DLVIDEOS
, ... - Disallows context menus or raises
OnContextMenu
event for each context menu activated. - Disallows accelerator keys or raises
OnAcceletorKeys
event for each accelerator key activated. - That, by default, is configured to take over user downloads using
FileDownloadEx
andOnFileDLxxxx
events. - That can be used as a simple download manager using
DownloadUrlAsync
method andOnFileDLxxx
events. - Allows fine tuning of security per URL via
SecurityManagerProcessUrlAction
event. - Allows interception and overriding of HTTP security problems via
OnHTTPSecurityProblem
event. - Allows interception and overriding of basic authentication requests via
OnAuthentication
event. - Allows replacing or augmenting registry settings via
OnGetOptionKeyPath
andOnGetOverrideKeyPath
events. - Allows posting of data via
GET
orPOST
methods with notifications viaOnPostxxxx
events. - Allows handling of single or multiple drops via
OnWBDragxxx
andOnWBDropx
events. - That adds a host of new properties, methods and events, in addition to almost any web browser wrapper control's properties, methods and events.
Background
The control is written in VC++ 6.0 using ATL 3.0. It is compiled with minimum dependencies (no MFC, std::
, CString
, ...). It is designed to host multiple web browser controls within one ATL created window. This is contradictory to MSDN recommendation which suggests to use one ATL window per hosted control. The reason for choosing this approach was to remove the burden of web browser control management from the hosting client application to the control. Normally, a developer places an instance of a control on a form/dialog, then if needed, an array of the controls is created and maintained by the client application. My approach enables the developer to insert one instance of this control on a form/dialog and then use CvbWB::AddBrowser
and CvbWB::RemoveBrowser
methods to add and remove web browser controls. Each newly created control (through an instance of IWB
class) is assigned a unique ID (wbUID)
. This unique ID enables the client application to communicate with that specific web browser control instance via its properties, methods, and to find out which web browser control has fired an event.
Even though this control was made to be used by VB, due to the fact that it is a fully compliant ActiveX control, it can also be used from MFC.
Implementation challenges
There are many articles that cover the basics of creating, hosting and sinking events of a web browser control. So rather than going through CoCreateInstance
, IOleObject::SetClientSite
, and so on, I decided to explain some of the main implementation challenges where you will find very little and often no information about them.
Here is a list of the main challenges encountered and resolved during the development of this control. I neither claim that these solutions are unique nor the best. Just that they seem to work.
Events
- The first issue that I encountered was lack of documentation on how to pass the
byref
parameter or objects to a client application such as VB. Attempts to handle any of these wizard generated events was causing GPF. Here is a sample of the non-working code forNewWindow3
event taken from theCProxy_IvbWBEvents
class:VOID Fire_NewWindow3(SHORT wbUID, IDispatch * * ppDisp, VARIANT_BOOL * Cancel, LONG lFlags, BSTR sURLContext, BSTR sURL) { T* pT = static_cast<T*>(this); int nConnectionIndex; CComVariant* pvars = new CComVariant[6]; int nConnections = m_vec.GetSize(); for (nConnectionIndex = 0; nConnectionIndex < nConnections; nConnectionIndex++) { pT->Lock(); CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex); pT->Unlock(); IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p); if (pDispatch != NULL) { pvars[5] = wbUID; ///////////////////////////////////////////////////// //Next two params need to be passed by ref or they //cause GPF // pvars[4] = ppDisp; pvars[3] = Cancel; ///////////////////////////////////////////////////// pvars[2] = lFlags; pvars[1] = sURLContext; pvars[0] = sURL; DISPPARAMS disp = { pvars, NULL, 6, 0 }; pDispatch->Invoke(0x2d, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp, NULL, NULL, NULL); } } }
And here is the correction:
pvars[4].vt = VT_BYREF|VT_DISPATCH; pvars[4].byref = ppDisp; pvars[3].vt = VT_BOOL|VT_BYREF; pvars[3].byref = Cancel;
- The second issue that I encountered was again related to events. This issue showed up when I implemented the protocol handlers. Apparently, the
IConnectionPointImpl
does not fire events across COM components. So, after some looking around, I came across the KB article 280512,ATLCPImplMT
encapsulates the ATL event firing across COM apartments. Using the included classIConnectionPointImplMT
(from MS) solved this issue. And here is the completed code for theNewwindow3
event:VOID Fire_NewWindow3(SHORT wbUID, IDispatch * * ppDisp, VARIANT_BOOL * Cancel, LONG lFlags, BSTR sURLContext, BSTR sURL) { T* pT = static_cast<T*>(this); int nConnectionIndex; CComVariant* pvars = new CComVariant[6]; int nConnections = m_vec.GetSize(); for (nConnectionIndex = 0; nConnectionIndex < nConnections; nConnectionIndex++) { ///////////////////////////////////////////////////////// //Next three lines need to be replaced //pT->Lock(); //CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex); //pT->Unlock(); ///////////////////// ///////////////////////////////////////////////////////// //Replaced the previous three lines //of code with the next two lines CComPtr<IUnknown> sp; sp.Attach (GetInterfaceAt(nConnectionIndex)); ///////////////////// IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p); if (pDispatch != NULL) { pvars[5] = wbUID; pvars[4].vt = VT_BYREF|VT_DISPATCH; pvars[4].byref = ppDisp; pvars[3].vt = VT_BOOL|VT_BYREF; pvars[3].byref = Cancel; pvars[2] = lFlags; pvars[1] = sURLContext; pvars[0] = sURL; DISPPARAMS disp = { pvars, NULL, 6, 0 }; pDispatch->Invoke(0x2d, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp, NULL, NULL, NULL); } } }
- The third issue showed up after implementing the protocol handler. I needed to fire events from the
WBPassthruSink
(protocol handler sink) class for a specific web browser control to notify the client application viaProtocolHandlerOnBeginTransaction
andProtocolHandlerOnResponse
events. Since the instances of theWBPassthruSink
class are created byURLMon
as needed byPassthroughAPP
, I had to find a way to determine which instance of the web browser control is involved so that I can fire events for that specific control. My solution was to find the Internet Explorer ServerHWND
in the implementation ofWBPassthruSink::OnStart
using theIWindowForBindingUI
interface obtained from our protocol handler://Using IWindowForBindingUI interface CComPtr<IWindowForBindingUI> objWindowForBindingUI; //This is a macro for QueryService HRESULT hret = QueryServiceFromClient(&objWindowForBindingUI); if( (SUCCEEDED(hret)) && (objWindowForBindingUI) ) { HWND hwndIEServer = NULL; //Should return InternetExplorerServer HWND objWindowForBindingUI->GetWindow(IID_IHttpSecurity, &hwndIEServer); //From here we can find the ATL window //hosting this instance of our control //and have it fire an event for the form/dlg hosting //this instance of our control if(hwndIEServer)
_ATL_MIN_CRT
One of the design goals of this control was to be build with minimum dependencies. Standard _ATL_MIN_CRT
support does the job for eliminating the CRT overhead well. But unfortunately, it doesn't support the use of global C++ constructs, like the following:
class CTest { public: CTest() { MessageBox(NULL, _T("Hello, I'm intitialized"), _T("Static object"), MB_SETFOREGROUND | MB_OK); } ~CTest() { MessageBox(NULL, _T("Bye, I'm done"), _T("Static object"), MB_SETFOREGROUND | MB_OK); } }; static CTest g_test; extern CTest *gp_Test;
The above would lead to linker conflicts, because the CRT code for constructors/destructors invocation would be referenced. To overcome this, I am using AuxCrt.cpp custom _ATL_MIN_CRT
implementation and a replacement for the AtlImpl.cpp class. This class is from Andrew Nosenko (andien@geocities.com). With this class, I was able to use CSimpleArray
as a global variable to keep track of the instances of web browser controls. Note, for convenience, I placed a copy of AuxCrt.cpp in the ATL\Include\ directory.
//Taken from StdAfx.h //gCtrlInstances keeps track of each instance of our control //This global is needed due to the fact that a client may place //this control on more than one form/dlg //or have multiple instances of BW //hosting in one control. In this case, using one //global ptr to our control will cause the events to be routed to the //first control, not the one we want. The control instances (this) is //added to this array in Constructor and //removed in Destructor of CvbWB class. extern CSimpleArray<void*> gCtrlInstances; //Flag to track registering and unregistering //of HTTP/HTTPS protocols //Can only be done once per DLL load. //Effects all instances of Webbrowser control. extern BOOL gb_IsHttpRegistered; extern BOOL gb_IsHttpsRegistered; //Protocol handling registration extern CComPtr<IClassFactory> m_spCFHTTP; extern CComPtr<IClassFactory> m_spCFHTTPS; ....
Asynchronous pluggable protocols
One of my main design goals was to be able to act as a pass through between a web browser control and URLMon
so as to intercept all the requests and responses by using an asynchronous pluggable protocol. At the start, this task seemed pretty straightforward, implementing IInternetProtocol
, IInternetProtocolInfo
, IInternetPriority
, IInternetProtocolSink
, IInternetBindInfo
, and IClassFactory
interfaces. In the implementation of the IServiceProvider::QueryService
, create and pass an instance of my IInternetProtocolImpl
to URLMon
. Overwrite the necessary methods and handle the requests. Unfortunately, this approach had a big flaw. My IInternetProtocol
implementation was only being called to handle the main document and not for the rest of the page requests, images, CSS,...
After doing some searching, I came across an excellent package called PassthroughAPP by Igor Tandetnik. In Google groups, under microsoft.public.inetsdk.programming.xxx, just search for PassthroughAPP. The package contains five files:
PassthroughObject.h | A simple COM object IPassthroughObject . |
ProtocolCF.h | A customized COM class factory, implements CComClassFactory . |
ProtocolCF.inl | Class factory implementation. |
ProtocolImpl.h |
Protocol handlers header. Implemented interfaces:
|
ProtocolImpl.inl | Protocol handlers implementation. |
Except for the five methods that I have overridden in the WBPassthruSink
class to intercept all the requests and response, I will not be able to answer any questions regarding PassthroughAPP. Please direct your questions to the author as my understanding of the package's design and implementation is limited.
RegisterBindStatusCallback and content-disposition header
One of the design goals of this control was to take full control over file downloads. This was pretty simple to implement at first and all seemed to work without a glitch. But as usual, a strange problem was reported. If a user attempted to download an attachment from Hotmail, Yahoo!, ... the default download dialog was being displayed, bypassing my custom download manager. I traced the problem to RegisterBindStatusCallback
method which is called in IDownloadManager::Download
method, it was returning E_FAIL
. This failure seems to occur when a server sends a content-disposition header in response to a file download request. Of course, MSDN does not even mention anything about an E_FAIL
return or why RegisterBindStatusCallback
might fail. After searching for a while to no avail, I decided to attempt to implement a workaround.
The first step was to somehow force RegisterBindStatusCallback
to succeed:
- Call
RegisterBindStatusCallback
, passing anIBindStatusCallback
pointer to retrieve the previousIBindStatusCallback
. - If the return value is
E_FAIL
, then callRevokeObjectParam
to un-register the previousIBindStatusCallback
. - If the return value from
RevokeObjectParam
indicates success, attempt to callRegisterBindStatusCallback
for a second time which should succeed.
//Taken from WBDownLoadManager::Download //filedl is an instance of my IBindStatusCallback implementation //pbc is a BindCtx pointer passed to Download method IBindStatusCallback *pPrevBSCB = NULL; hr = RegisterBindStatusCallback(pbc, reinterpret_cast<IBindStatusCallback*>(filedl), &pPrevBSCB, 0L); if( (FAILED(hr)) && (pPrevBSCB) ) { //RevokeObjectParam for current BSCB, so we can register our BSCB //_BSCB_Holder_ is the key used to register //a callback with a specific BindCtX LPOLESTR oParam = L"_BSCB_Holder_"; hr = pbc->RevokeObjectParam(oParam); if(SUCCEEDED(hr)) { //Attempt register again, should succeed now hr = RegisterBindStatusCallback(pbc, reinterpret_cast<IBindStatusCallback*>(filedl), 0, 0L); if(SUCCEEDED(hr)) { //Need to pass a pointer for BindCtx //and previous BSCB to our implementation filedl->m_pPrevBSCB = pPrevBSCB; filedl->AddRef(); pPrevBSCB->AddRef(); filedl->m_pBindCtx = pbc; pbc->AddRef(); } //....
The second step was to relay some calls to the previous IBindStatusCallback
from our implementation. Otherwise, no download will take place. Memory leaks and crashes are to be expected. This part was based on trial and error.
- In
::OnStartBinding
:if(m_pPrevBSCB) { m_pPrevBSCB->OnStopBinding(HTTP_STATUS_OK, NULL); }
- In
::OnProgress
:if(m_pPrevBSCB) { //Need to do this otherwise a //filedownload dlg will be displayed //as we are downloading the file. if(ulStatusCode == BINDSTATUS_CONTENTDISPOSITIONATTACH) return S_OK; m_pPrevBSCB->OnProgress(ulProgress, ulProgressMax, ulStatusCode, szStatusText); }
- In
::OnStopBinding
:if( (m_pPrevBSCB) && (m_pBindCtx) ) { //Register PrevBSCB and release our pointers LPOLESTR oParam = L"_BSCB_Holder_"; m_pBindCtx->RegisterObjectParam(oParam, reinterpret_cast(m_pPrevBSCB)); m_pPrevBSCB->Release(); m_pPrevBSCB = NULL; m_pBindCtx->Release(); m_pBindCtx = NULL; //Decrease our ref count, so when release is called //we delete this object --m_cRef; }
Brief overview of the classes
Please ensure that you have the latest SDK that works with VC++ 6.0, February 2003 SDK, and IE6 headers and libraries.
Class name | Implements | Description |
---|---|---|
CvbWB |
|
This class was created as a full ATL control using the wizard. It is responsible to host the control in a client application (VB, C++), fire events, and allow access to properties and methods of all web browser controls to the hosting client. This task is achieved by using a simple array of |
IWB |
IUnknown |
This class is responsible to create and maintain a single instance of a web browser control along with all the necessary classes such as WBClientSite (IOleClientSite implementation). Also, all the QIs from all the classes are routed through the same IWB instance that created them. In addition, it contains a number of useful methods, IsFrameset , FramesCount , DocHighlightFindText ,... |
WBClientSite |
IOleClientSite |
Required as part of web browser control hosting interfaces. All methods return E_NOTIMPL . |
WBInPlaceSite |
IOleInplaceSite |
Required as part of web browser control hosting interfaces. All methods return E_NOTIMPL . |
WBEventDispatch |
IDispatch |
This class is the sink for web browser control events. As the events arrive from the web browser control, it fires events to notify the client application. |
WBDocHostShowUI |
IDocHostShowUI |
I am only handling Your current security settings prohibit running ActiveX controls on this page. As a result, the page may not display correctly. |
WBOleCommandTarget |
IOleCommandTarget |
The purpose of this class is to intercept script errors via ::Exec method, and based on the m_lScriptError flag, either allow or disallow them. |
WBAuthenticate |
IAuthenticate |
This class intercepts requests for basic authentication from servers, and notifies the client using OnAuthentication event to obtain the username and password. Useful for clients wanting to automate the process of logging in using basic authentication schemes. |
WBDocHostUIHandler |
IDocHostUIHandler |
The purpose of this class is to intercept the context menu (::ShowContextMenu ), accelerator keys (::TranslateAccelerator ), and to set the UI flags (::GetHostInfo ). |
WBHttpSecurity |
IHttpSecurity |
This class intercepts HTTP related security problems, such as ERROR_HTTP_REDIRECT_ NEEDS_CONFIRMATION *, ERROR_INTERNET_SEC_ CERT_CN_INVALID * via the OnSecurityProblem method. Please note, using the ::OnSecurityProblem method or OnHTTPSecurityProblem event incorrectly can compromise the security of your application and potentially leave users of your application exposed to unwanted information disclosure. |
WBSecurityManager |
IInternetSecurityManager |
This class only implements the ::ProcessUrlAction method. It returns INET_E_DEFAULT_ACTION for the rest of the methods. Please note, using ::ProcessUrlAction method or SecurityManagerProcess UrlAction * event incorrectly may result in the incorrect processing of URL actions and possibly leave users susceptible to elevation of privilege attacks. |
WBServiceProvider |
IServiceProvider |
It is responsible to respond to the QueryService calls on our IUknown(IWB) in the ::QueryService method. |
WBWindowForBindingUI |
IWindowForBindingUI |
It returns a handle to a window via the ::GetWindow method which is used by MSHTML to display information in the client's user interface when necessary. Currently, this method returns a handle to the Internet Explorer server window. |
WBBSCBFileDL |
IBindStatusCallback IHttpNegotiate |
An instance of this class is created and used to receive callbacks and notify the client app via OnFileDLxxxx events for all the file downloads, by the user clicking on a download link or by using ::DownloadUrlAsync method from the code. |
WBDownLoadManager |
IDownloadManager |
It implements ::Download method which in turn creates an instance of our IBindStatusCallback implementation (WBBSCBFileDL ), registers our BSCB for callbacks and notifies the client via OnFileDLxxxx events of the progress of the download. Each BSCB is given a unique ID and a pointer to it is stored in a simple array in the CvbWB class instance. This ID can be used by the client app to cancel a download by calling CancelFileDl passing the ID. |
CTmpBuffer |
A simple string buffer class, since CString is not available due to the minimum dependency requirement. | |
CUrlParts |
A simple class which uses the InternetCrackUrl method of WinInet to break a given URL into its parts. This includes file name and extension, if available. | |
WBPassThruSink |
CInternetProtocolSinkWithSP IHttpNegotiate |
This class is the sink for the protocol handlers. |
WBDropTarget |
IDropTarget |
To handle custom dragdrop. |
WBDocHostUIHandler |
IDocHostUIHandler2 |
To handle GetOverrideKeyPath method. |
WBStream |
IStream |
To handle uploads with progress. |
* Ignore the space that has been added to avoid page scrolling.
Setting up the control
- Copy vbMHWB.dll located in the Binaries sub folder to your system directory.
- Register vbMHWB.dll using regsvr32.exe.
- Open VBDemo or MFCDemo project.
How to register, example:
Assuming the system dir path is 'C:\windows\system32\':
regsvr32.exe C:\windows\system32\vbMHWB.dll.
About demo applications
Both demo projects are almost identical in terms of GUI and their use of the control. The VBDemo was built using Visual Basic 6.0 and has no dependencies other than this control. The MFCDemo project was built using Visual C++ 6.0 and also has no other dependencies. With MFCDemo, you need to treat BOOL
as VARIANT_BOOL
(wizard translates VARIANT_BOOL
as BOOL
), and make sure that the value of an in/out BSTR
parameter is released (ClearBSTRPtr
method is provided as an example) before assigning a new value. This step is necessary to avoid memory leaks.
Build versions have been included in the Binaries subfolder for both projects.
Control information
A list of new or modified properties, methods (90), and events (40) along with a log of changes has been included in vbMHWB.htm file.
Related documents
- WebBrowser Customization
- WebBrowser Control Reference for C/C++ Developers
- Advanced Hosting Reference
- How to Get Protocol Headers in a Pluggable Protocol Handler
- SAMPLE: ATLCPImplMT encapsulates ATL event firing across COM apartments
- How to handle script errors as a WebBrowser control host
- About Asynchronous Pluggable Protocols
- URL Monikers Interfaces
- HTML Control API Interfaces
- Implementing a Custom Download Manager
- MSHTML Reference
- IInternetSecurityManager
- Programming with ATL and C Run-Time Code
History
Please see vbMHWB.htm for a list of changes.
- 15th March, 2006 - Current version (1.2.1.3).
- 13th May, 2005 - Initial version (1.0.0.1) posted.