Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

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

0.00/5 (No votes)
16 Mar 2006 1  
An article on WebBrowser hosting and customization.

Index

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 and OnFileDLxxxx events.
  • That can be used as a simple download manager using DownloadUrlAsync method and OnFileDLxxx 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 and OnGetOverrideKeyPath events.
  • Allows posting of data via GET or POST methods with notifications via OnPostxxxx events.
  • Allows handling of single or multiple drops via OnWBDragxxx and OnWBDropx 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.

Index

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

  1. 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 for NewWindow3 event taken from the CProxy_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;
  2. 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 class IConnectionPointImplMT (from MS) solved this issue. And here is the completed code for the Newwindow3 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);
            }
        }
    }
  3. 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 via ProtocolHandlerOnBeginTransaction and ProtocolHandlerOnResponse events. Since the instances of the WBPassthruSink class are created by URLMon as needed by PassthroughAPP, 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 Server HWND in the implementation of WBPassthruSink::OnStart using the IWindowForBindingUI 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)

Index

_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;
....

Index

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:

  • IPassthroughObject
  • IInternetProtocol
  • IInternetProtocolInfo
  • IInternetPriority
  • IInternetThreadSwitch
  • IWinInetHttpInfo
  • IInternetProtocolSink
  • IServiceProvider
  • IInternetBindInfo
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.

Index

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 an IBindStatusCallback pointer to retrieve the previous IBindStatusCallback.
  • If the return value is E_FAIL, then call RevokeObjectParam to un-register the previous IBindStatusCallback.
  • If the return value from RevokeObjectParam indicates success, attempt to call RegisterBindStatusCallback 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;
    }

Index

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
  • CComObjectRootEx
  • IDispatchImpl
  • CComControl
  • IPersistStreamInitImpl
  • IOleControlImpl
  • IOleObjectImpl
  • IOleInPlace ActiveObjectImpl*
  • IViewObjectExImpl
  • IOleInPlaceObject WindowlessImpl*
  • ISupportErrorInfo
  • IConnectionPoint ContainerImpl*
  • IPersistStorageImpl
  • ISpecify PropertyPagesImpl*
  • IQuickActivateImpl
  • IDataObjectImpl
  • IProvideClassInfo2Impl
  • IPropertyNotifySinkCP
  • CComCoClass
  • CProxy_IvbWBEvents

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 pointers. The pointers are added and removed from the array by calls to AddBrowser and RemoveBrowser methods. Each new instance of IWB is given a unique ID wbUID. The client application uses this ID to access this instance of the web browser control's properties and methods. Also, all the events come with an extra parameter, wbUID, which identifies the web browser instance that has fired the event. In addition, this class contains a number of useful methods and properties, get_ActiveDocumentObj, get_ActiveElementObj (returns active document or element; accounts for frames), DownloadUrlAsync (starts a file download, allowing client app to monitor the status via OnFileDL_xxx events), ucInternetCrackUrl (to break a given URL into parts including filename and extension, and the parts are accessed via Get/Set ucXXX properties),...

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 ::ShowMessage method which is responsible to intercept HTTP messages and notify the client via the Fire_ShowMessage event to determine what to do with the message. A typical message may look like this:

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.

Index

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.

Index

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

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.

Index

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here