Common Articles and Tutorials

WebBrowser Control

Author: Alexander Stoyan
Platform: Visual C++, Visual Basic 6.0, Visual C#, Visual Basic .NET

In version 22 both ToolkitPro and SuitePro have introduced a new WebBrowser control in order to satisfy a growing demand in full support of modern web tools and techniques. While the new WebBrowser control is merely a wrapper around system provided web browser components, it provides a unique ability to choose which web browser component to use and provides a narrow unified interface to interact with that component. In its initial release it comes with ShellExplorer web browser provider used by Microsoft Internet Explorer, and WebView2 web browser provider used by Microsoft Edge. This article explains the basics and specifics of using the new WebBrowser control and its available web browser providers, namely:

  1. Control Distribution
  2. Control Instantination
  3. Changing Provider
  4. Customization
  5. Event Handling
  6. Scripting


Control Distribution

Depending on the desired control usage it may be necessary to distribute additional provider specific files and modules. The lack of such files won't make the control non-functional but supported provider types may become unavailable.

The ShellExplorer provider type is assumed to be an essential part of a system as it's normally pre-installed on all supported platforms for both x86 and x64 configurations. This web browser provider does not require distributing any additional files.

The WebView2 provider type requires the WebView2Loader32.dll module for x86 configurations and WebView2Loader64.dll module for x64 configurations to be included along with the executable application module, or in case SuitePro the .OCX module. In addition, the WebView2 components have to be installed on the target computer. Normally, the presence of Microsoft Edge on the target computer means that WebView2 components are available, thus starting from Windows 10 the WebView2 components can be assumed built-in and always available, but the earlier versions of Windows won't have it by default or may not have it at all. The required WebView2Loader32/64.dll modules are included into ToolkitPro and SuitePro installation packages and can be found in Source\Controls\WebBrowser\Providers\WebView2 relatively to the ToolkitPro root installation directory. The SuitePro package currently includes them in the samples only, like C:\Program Files (x86)\Codejock Software\ActiveX\Xtreme SuitePro ActiveX v22.0.0\Samples\Controls\C#\SuiteControls1. The future versions will have them in the Bin directory relatively to the root SuitePro installation directory.

ToolkitPro users have an option to link WebView2LoaderStatic.lib static library in order to avoid WebView2Loader32/64.dll modules distribution by re-compiling ToolkitPro with XTP_USE_WEBVIEW2_STATIC_LIB macro enabled in the ToolkitPro C++ project settings for the needed configurations, or by stripping comments from the line #define XTP_USE_WEBVIEW2_STATIC_LIB in Source\Controls\WebBrowser\Providers\XTPWebViewProvider.h relatively to the root ToolkitPro installation location, however doing so has known issues. At the time of creating this article the latest available version of WebView2LoaderStatic.lib provided by Microsoft is incompatible with Visual C++ compilers prior to version that is included into Visual Studio 2015 (_MSC_VER < 1900). The 2015 and 2017 Visual C++ compliers can be used with those libraries but debug information symbols must be disabled for both ToolkitPro and the target projects in both compiler and linker settings. Not doing so will result in an invalid executable file to be generated! Starting from Visual C++ 2019 compiler (_MSC_VER >= 1920) the XTP_USE_WEBVIEW2_STATIC_LIB macro can be used in all configurations without any restrictions.


Control Instantiation

By default, the WebBrowser control tries to set the newest available provider type, which is currently WebView2. TookitPro users can use of the XTPWebBrowserProvider enumeration constants as an argument for the CXTPWebBrowserCtrl constructor in order to use another available web browser providers, but if either the default or user specified provider type is not available it will always fall back to the ShellExplorer provider type.

It is recommended to set the needed provider type and customize it if necessary, prior to creating the WebBrowser control window or binding it to a form control. This way the unnecessary provider initialization will be avoided. Changing provider type after creating control window or binding it to a window is possible but may affect performance as it implies releasing the previously allocated browser resources, unloading provider specific modules, loading the new provider type specific modules and allocating new resources.

After the control is successfully instantiated and, if necessary, configured, the Navigate method can be called for loading the specified web page using the current provider:

wndBrowser.Navigate("https://codejock.com");

Changing Provider

As mentioned above, the newest available provider type is set for the WebBrowser control by default, or it falls back to the ShellExplorer provider type which is assumed to be always available. However, there are cases when changing the provider may be necessary at runtime, i.e., requested by the user in application settings or in order to display a certain content that cannot be displayed using the current provider type. It can be easily achieved using the CXTPWebBrowserCtrl::SetProviderType method for ToolkitPro users and setting the ProviderType property for SuitePro users. ToolkitPro users can verify the return value of the SetProviderType method, it will be TRUE if the operation succeeds or FALSE otherwise. SuitePro users will have to check the value of the ProviderType property right after changing it in order to make sure it's not changed automatically to xtpWebBrowserProviderShellExplorer, which would mean a fallback logic as a result of failed provider type changing.

Changing provider type does not affect WebBrowser control properties set via SetProperty method, but upon changing, a new provider type may check for certain properties and use their values if specified, so it is important to set the needed properties prior to changing provider type.


Customization


WebView2 User Data Folder location (starting from version 22.1)

WebView2 applications use User Data Folders (UDFs) to store browser data, such as cookies, permissions, and cached resources. WebView2 requires the UDF directory to exist, so if it does not exist then component will attempt to create it either at the default or specified location. If due to any reason the UDF directory cannot be created or accessed the component will show an error message like below and may not function properly:

Microsoft Edge can't read and write to its data directory

The default UDF directory location is always the full path to the executable with .WebView2 added at the end. Thus, if the UDF directory location is not specified and the executable is stored in the Program Files directory, any attempt to run the application without Administrator privileges will result in that error message due to a lack of write permissions to Program Files directory. In this case it may be necessary to set a custom location for the UDF directory. Both ToolkitPro and SuitePro users can do it by setting the "UDF" property using the SetProperty method with a string value of the full path where the UDF directory already exists or is to be created, for example:

string profilePath = Environment.ExpandEnvironmentVariables(@"%USERPROFILE%\Codejock\Samples\C#\SuiteControls");

if (!Directory.Exists(profilePath))
    Directory.CreateDirectory(profilePath);

WebBrowser.SetProperty("UDF", profilePath);

In the result of the executing the code above will be creation of %USERPROFILE%\Codejock\Samples\C#\SuiteControls\SuiteControls.exe.WebView2 UDF directory.

The "UDF" property must be set prior to creating the WebBrowser control window or binding its instance to a window, or prior to setting provider type to xtpWebBrowserWebView in order to avoid performance penalties. If for some reason the "UDF" directory is set after WebView2 provider is set and the control is created then the only way to apply the change is to re-create provider instance itself by calling the ReCreateProvider method. Doing so will result in page reloading, all unsaved data in the page lost and all internal connections or references invalidated.


User-Agent HTTP header (starting from version 22.1)

There are cases when a web-server has to generate an application specific HTML code or perform a certain application specific logic if detected that a web-page is loaded from an application. One of the conventional ways to achieve such a functionality is to set an application specific User-Agent HTTP header that can be easily checked by the web-server. The default value of User-Agent HTTP header in the WebBrowser control depends on the active web browser provider, i.e. if WebView2 provider is active then by default the backend will see it as Microsoft Edge browser specified by the string. Here is what the string the looks like:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.70

If the ShellExplorer provider is active then the backend will see its User-Agent as:

Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko

Using the WebBrowser control it is simple to set a custom User-Agent string for all navigated pages and in-page link clicks using the "UserAgent" property, the value of which must be a new non-empty string that conforms with RFC-7231 requirements for HTTP header values and security considerations.

This example shows settings a custom User-Agent value:

WebBrowser.SetPropert("UserAgent", "Codejock Software Sample");

The customized User-Agent value will keep its value across changing web browser providers and can be set to its default value by specifying null, Nothing or an empty string value.


ShellExplorer registry settings customization (starting from version 22.1)

By default, the ShellExplorer component keeps its settings in the HKEY_CURRENT_USER/Software/Microsoft/Internet Explorer registry key. There are cases when those settings have to be complemented or overridden altogether without affecting other instances of the ShellExplorer component and the Internet Explorer itself. It can be achieved using the "OptionKeyPath" and "OverrideKeyPath" properties. Both those properties take their values as string values of the new custom registry location relatively to HKEY_CURRENT_USER.

The "OptionKeyPath" property is used for specifying a registry path to read an overridden settings from, i.e., if a setting is not found at the specified custom registry location then it will be read from the default settings location. The specified value is used internally as a value returned from the IDocHostUIHandler::GetOptionKeyPath method implementation.

The specified "OverrideKeyPath" property tells ShellExplorer that the default settings location should be ignored, and the provided registry path must be used as a sole location for all settings. The specified value is used internally as a value returned from the IDocHostUIHandler2::GetOverrideKeyPath method implementation.

string subKey = @"Software\Codejock Software ActiveX Demos\C#\SuiteControls\WebBrowser\OverrideKeyPath";
Registry.CurrentUser.CreateSubKey(subKey);
WebBrowser.SetProperty("OverrideKeyPath", subKey);

Additional customizations available for ToolkitPro users only

The user can supply its own COM interface implementation to the active provider to be used as a part of the browser control client site. Whenever a provider specific web browser implementation queries its client site for an interface, the list of user provider client interfaces will be examined first, and should the requested interface be found in the list, its reference will be returned to the web browser implementation. The following below example demonstrates how to override and provide a custom implementation of the IOleCommandTarget interface for the ShellExplorer provider type:

class CCustomOleCommandTarget : public CCmdTarget
{
public:
    CCustomOleCommandTarget(CXTPWebBrowserCtrl& webBrowser)
    : m_webBrowser(webBrowser)
    , m_pBrowserObjUnk(m_webBrowser.GetProvider()->GetBrowserObject())
    {
        ASSERT(NULL != m_pBrowserObjUnk);
        m_pBrowserObjUnk->AddRef();
    }

    ~CCustomOleCommandTarget()
    {
        m_pBrowserObjUnk=>Release();
    }

protected:
    DECLARE_INTERFACE_MAP();

    BEGIN_INTERFACE_PART(OleCommandTarget, IOleCommandTarget)

    virtual HRESULT STDMETHODCALLTYPE QueryStatus(const GUID* pguidCmdGroup, ULONG cCmds,
        OLECMD prgCmds[], OLECMDTEXT* pCmdText)
    {
        METHOD_PROLOGUE(CCustomOleCommandTarget, OleCommandTarget);

        // TODO: some implementation
        return S_OK;
    }

    virtual HRESULT STDMETHODCALLTYPE Exec(const GUID* pguidCmdGroup, DWORD nCmdID,
        DWORD nCmdexecopt, VARIANT* pvaIn, VARIANT* pvaOut)
    {
        METHOD_PROLOGUE(CCustomOleCommandTarget, OleCommandTarget);

        // TODO: some implementation
        return S_OK;
    }

    END_INTERFACE_PART(OleCommandTarget)

private:
    CXTPWebBrowserCtrl& m_webBrowser;
    IUnknown* m_pBrowserObjUnk;
};

BEGIN_INTERFACE_MAP(CCustomOleCommandTarget, CCmdTarget)
    INTERFACE_PART(CCustomOleCommandTarget, __uuidof(IOleCommandTarget), OleCommandTarget)
END_INTERFACE_MAP()

XTP_IMPLEMENT_IUNKNOWN(CCustomOleCommandTarget, OleCommandTarget);

...
...
...

// Somewhere in the code the IOleCommandTarget implementation is provided
// to the ShellExplorer web browser provider.
ASSERT(xtpWebBrowserShellExplorer == webBrowser.GetProviderType());

CCustomOleCommandTarget* pCmdTarget = new CCustomOleCommandTarget(*this);
webBrowserCtrl.GetProvider()->SetClientInterface(__uuidof(IOleCommandTarget),
                                                 pCmdTarget->GetControllingUnknown());
pCmdTarget->GetControllingUnknown()->Release();

If direct access to the provider specific web browser is needed for browser specific operations, the CXTPWebBrowserProvider::GetBrowserObject() method should be used for obtaining the IUnknown interface of the browser object. For ShellExplorer provider it can be cast to the IWebBrowser2 interface, and for WebView2 provider respectively, it can be cast to the ICoreWebView2Controller interface.


Event Handling

The WebBrowser control subscribes to all events of the provider specific web browser object automatically upon successful provider initialization. Every time an event is triggered, the control calls either the default or the provided event handler. Each event handler name is prefixed with the name of the applicable provider, e.g. ShellExplorer_OnStatusTextChange. SuitePro users simply need to subscribe to the needed events exposed by the control. But ToolkitPro users have to implement the IXTPWebBrowserEventSink interface and provide implementation of the events needed to be handled and bind it to the control using CXTPWebBrowserCtrl::SetEventSink method. All event handlers in IXTPWebBrowserEventSink have empty implementation for convenience.


Scripting

The WebBrowser control unifies JavaScript execution in the current page context and handling values returned from the executed scripts. The simplest use case that does not require interaction with the host application looks identically for both ToolkitPro and SuitePro users:

webBrowser.ExecuteScript("alert('Hello from JavaScript!')")

If a script is expected to return a value, it will be returned from the ExecuteScript method (via the optional COleVariant pointer argument for C++). The format of the returned value is not guaranteed to be consistent for all provider types, while ShellExplorer always returns a value in its native type, WebView2 returns certain data types as a JSON string that represents the serialized returned value, or an empty string if the value cannot be represented in JSON.

In order to interact with the host application from JavaScript it is necessary to bind a scripting enabled object with the external JavaScript global property. SuitePro users should call SetObjectForScripting method to specify such and object:

Me.SetObjectForScripting Me

ToolkitPro user should call CXTPWebBrowserCtrl::SetExternal and specify an IDispatch pointer of an object that exposes named properties and methods for scripting:

class CExternalObject : public CCmdTarget
{
public:
    CExternalObject() { EnableAutomation(); }

protected:
    DECLARE_DISPATCH_MAP();
    
    long OleTickCount() { return static_cast(::GetTickCount()); }
};

BEGIN_DISPATCH_MAP(CExternalObject, CCmdTarget)
    DISP_PROPERTY_EX(CExternalObject, "tickCount", OleTickCount, SetNotSupported, VT_I4)
END_DISPATCH_MAP()

...
...
...
// Somewhere in the code a custom implementation of 
// the 'external' object is supplied to the WebBrowser control.
CExternalObject* pExternalObj = new CExternalObject();
webBrowser.SetExternal(pExternalObj->GetIDispatch(FALSE));
pExternalObj->GetControllingUnknown()->Release();

CComVariant vtResult;
webBrowser.ExecuteScript(
    L"(function() {\n"
    L"    var tickCount = external.tickCount;\n"
    L"    alert('Current tick counter: ' + tickCount);\n"
    L"    return tickCount;\n"
    L"})()", &vtResult);

vtResult.ChangeType(VT_BSTR);
AfxMessageBox(CString(_T("The script execution has returned: ")) + XTP_CW2CT(vtResult.bstrVal),
              MB_ICONINFORMATION);