Click here to Skip to main content
15,868,016 members
Articles / Web Development / HTML

Overcoming WebBrowser control scripting limitations with ScriptingBridge

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
10 Jul 2015MIT6 min read 15.6K   1   3   2
Tightly coupling .NET code and browser side JavaScript is difficult with the WebBrowser control. ScriptingBridge was designed to overcome these limitations.

 

Introduction

While the WebBrowser control works more or less out of the box for displaying simple web pages, it does have its quirks. Alternative embeddable WebBrowsers for .NET are becoming available more and more, but WebBrowser is the only one supported by Microsoft.

One key difficulty I found is creating a tight coupling between browser side JavaScript code and the .NET world hosting the WebBrowser control. To overcome the numerous limitations I developed the Scripting Bridge which I present to you here.

The key limitations of the WebBrowser’s Document.InvokeScript() function fixed by the ScriptingBridge are:

  • Calling functions on any object, not just in the global context.
  • Passing data back and forward without having to make it rain ComVisible attributes.
  • Did my void returning JS function get called or did an error occur?

Using the code

Initialize a single scripting bridge per WebBrowser control. The first parameter to the constructor is the WebBrowser control instance to bind to and the second parameter is a Boolean indicating if automatic initialization is to be performed. This would look as follows in your Form class:

C#
using Vereyon.Windows;

public partial class ExampleForm : Form
{

    public ScriptingBridge Bridge { get; private set; }

    public ExampleForm()
    {
        InitializeComponent();

        Bridge = new ScriptingBridge(webBrowser, true);
    }
}

Assuming that you webpage contains the following JavaScript section:

JavaScript
<script type="text/javascript">

var myObject = {

  myFunction: function(data)
  {

    alert(data.message);

    return {
      id: 12356,
      date: new Date(),
      title: 'Some string'
    };
  }
}

</script>

You can call the function myObject.myFunction() from the .NET code as follows:

C#
private void scriptingButton_Click(object sender, EventArgs e)
{

    var parameter = new ScriptingParameterData
    {
        Message = "Test message"
    };

    var data = Bridge.InvokeFunction<ScriptingReturnData>("myObject.myFunction", parameter);
}

Where the ScriptingParameterData class is defined as follows:

C#
public class ScriptingParameterData
{
    public string Message { get; set; }
}

Frequently Asked Questions

How do I know if the Scripting Bridge is initialized?

The ScriptingBridge object provided a property and an event for this purpose:

C#
public class ScriptingBridge
{

    /// <summary>
    /// Gets if the browser bridge is initialized.
    /// </summary>
    [ComVisible(false)]
    public bool IsInitialized { get; private set; }

    /// <summary>
    /// Invoked when the scripting bridge has been initialized.
    /// </summary>
    public event EventHandler Initialized;
}

I want to provide extra services to my code but ScriptingBridge is in the way.

Since you can set only once ScriptingObject and you are required to set it to a ScriptingBridge instance in order for the bridge to work you are left with no direct method to expose extra services to the web page.

A straightforward solution is to extend the ScriptingObject and to expose your services from there derived class.

What version of Internet Explorer is WebBrowser running?

This is a dreaded question for WebBrowser users and a lot has been written about controlling this version. The Scripting Bridge will magically tell you where in history you are after initialization:

C#
public class ScriptingBridge
{

    /// <summary>
    /// Gets the WebBrowser document mode (also known as the IE version).
    /// </summary>
    [ComVisible(false)]
    public string DocumentMode { get; private set; }
}

Background

This section will delve deeper in the design and inner workings of the Scripting Bridge. The following figure provides an overview of the functional design of the Scripting Bridge:

Image 1

The call gate

Central to the Scripting Bridge is the client side call gate. The call gate is tasked with decoding requests from the host, calling the actual JavaScript function and passing the result back to the host in a useful manner. In addition to this the call gate must be able to provide basic feedback to the host in case errors occur. It forms the glue between the host and the client side.

The call gate must make its presence known to the Scripting Bridge at page load. To this end the Scripting Bridge provides the following COM visible function:

C#
[ComVisible(true)]
public bool RegisterClient(string args)

The args parameter contains a JSON string which contains basic configuration and information about the client side environment. The most important parameter is the name under which the call gate is available on the global window object. This will be the function name Document.InvokeScript() will be calling behind the scenes.

Calling arbitrary methods

The Document.InvokeScript() method provided by the WebBrowser control has the major disadvantage that it can only call functions in the global context. It does not allow to call any member functions of objects.

The way to overcome this is to have the gall gate resolve the context of the object. By passing the target function in the format of “namespace.object.function” and splitting on the dots, the call gate obtains the following list of string pieces: { “namespace”, “object”, “function” }. The call gate assumes that the last piece is the name of the function and that any parts before it define the context. The context is resolved iteratively by first looking for the first piece “namespace” in the global context and then for “object” as a member of “namespace”, and so forth. In code this looks as follows:

JavaScript
// The root context is assumed to be the window object. The last part of the method parameter is the actual function name.
var context = window;
var namespace = method.split('.');
var func = namespace.pop();

// Resolve the context
for (var i = 0; i < namespace.length; i++) {
    context = context[namespace[i]];

       // Check if the context is defined so far.
       if(context == undefined)
       {
             return JSON.stringify({
                    success: false,
                    error: namespace.slice(0, i + 1).join('.') + ' is undefined.'
             });
       }
}

...

// Call the function.
result = context[func](args);

 

Injecting script dependencies

Given the design of the Scripting Bridge, it is required to ensure that the client side of the scripting bridge is available in the webpage. One way to ensure this is to include the code in every webpage using conventional means. In addition the JSON object may not be available when running Internet Explorer in some older compatibility modes.

In order to not have to deal with including these script in every viewed page in looked into a method to inject these scripts via the Scripting Bridge itself. It turned out that calling the JavaScript eval() function with the required script source code as an argument works pretty well.

Avoiding having to deal with COM objects

Passing objects using the Document.InvokeScript() function is perfectly possible. You just need to make sure that everything you pass is visible to COM using the ComVisible attribute and make sure that any member methods which should not be callable from the JavaScript code are properly made invisible.

I did not want to have to deal with this, thus I made the Scripting Bridge serialize any passed arguments as JSON. As an added bonus I could do away with having to put my arguments in an object array all the time when calling Document.InvokeScript().

When returning an object from client side JavaScript to the host side this data will be exposed as a COM object for anything that is not a primitive data type. While perfectly possible, working with COM objects is experienced as very challenging by many programmers and can be cumbersome.

I wanted to be able to pass arbitrary data from client side JavaScript to the host side without any hassle. In order to do so I decided to let the call gate serialize any returned data as JSON and to deserialize this data again on the host side. By making this implementation generic, it allows deserialization straight to a strongly typed data model without requiring further mapping.

Differentiating between void returned and errors

The Document.InvokeScript() has the annoying property that it returns null in case an error occurs. This makes it impossible to tell the difference between an error occurring and a function simply returning null or nothing. The correct execution of the following code for example would not be discernible from an error occurring:

JavaScript
function voidReturn()
{
     // Do some stuff
     return;
}

 

In order to deal with this, the client side of the scripting bridge wraps every function call result in an object explicitly indicating if an error occurred and the returned value. If an unrecoverable error occurs Document.InvokeScript() will return null and the host side of the scripting bridge can tell that something really went wrong.

Supporting custom JSON serialization settings

As mentioned ScriptingBridge leverages JSON to pass internal data structures between the client and host side. The great JSON.Net library from Newtonsoft is used to perform host side serialization and deserialization.

Frequently the mapping from JSON to .NET objects is not completely straight forward. Think about the exact object member naming conventions. Do you use camel case, or upper case, and is this equal in both your JavaScript and .NET code? To avoid forcing any specific convention on the user, the JSON serialization settings for the payload data are exposed via the JsonSerializerSettings property.

Tests

Does this all really work? Yes, see the included Vereyon.Windows.WebBrowser.Tests project for the xUnit based unit tests.

History

Find the latest version at Github: https://github.com/Vereyon/WebBrowser

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Netherlands Netherlands
Developer at AlertA contractbeheer.

Comments and Discussions

 
Questioncalling c# method from Javascript Pin
Alexbrons26-Jan-16 5:45
Alexbrons26-Jan-16 5:45 
AnswerRe: calling c# method from Javascript Pin
Christ Akkermans28-Jan-16 7:18
Christ Akkermans28-Jan-16 7:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.