Asynchronous interaction between a Windows Forms WebBrowser control and the hosted web page

By: on February 24, 2015

The System.Windows.Forms.WebBrowser control allows one to host a web browser inside a .NET application (the “hosting application”). The control exposes the ObjectForScripting property which allows an object on your application to be accessed by the web page hosted in the WebBrowser control through the Javascript window.external object.

Invoking code on the hosted web page from the hosting application is a little bit more involved. The WebBrowser.Document property allows access to the HtmlDocument hosted by the control, and has the InvokeScript method which allows you to call an arbitrary Javascript function on the hosted web page. This method, however, does not allow you to call a method on any Javascript object other than the window object, leading to some workaround code:

Wouldn’t it be nice if we could have the web page call the hosting application to perform some long running task asynchronously, and then return the result back to the web page later on without locking up the UI? We can achieve this using Tasks and jQuery’s Deferred Objects.

I will highlight an approach I have implemented for one of our projects.

A Javascript function would first call a method exposed by the hosting application via the window.external object. The hosting application is then expected to initialise the task, and then return a unique GUID quickly. The function would then create a deferred object, identified internally by the GUID, to its callers, promising to fulfil the deferred object in the future. Once the hosting application is done, it would have to call some callback function on the web page. The callback function would then resolve or reject the deferred object. You would need to establish a convention on how things will work.

Consider the following example Javascript code that might live on a hosted web page:

var externalObject = (function(){
    var deferrals = [];
    var failResponse = {
    success: false,
    error: "This can only be invoked when running inside a hosted application." };

    return {
        resolveCallback: function(guid, response) {
            if (typeof deferrals[guid] !== "undefined") {
                var deferral = deferrals[guid];
                deferral.resolveWith(deferral, [args]);
            }
        },
        rejectCallback: function(guid, response) {
            if (typeof deferrals[guid] !== "undefined") {
                var deferral = deferrals[guid];
                deferral.rejectWith(deferral, [args]);
            }
        }
        doSomething: function(params) {
            var deferral = $.Deferred();
            if (typeof window.external.upload !== "undefined") {
                var guid = external.doSomething(JSON.stringify(params));
                deferrals[guid] = deferral;
            } else {
                deferral.rejectWith(deferral, [failResponse]);
            }
            return deferral.promise();
        }
    }
}());

externalObject("something")
    .done(function(response) {
        alert("Success!");
    }).fail(function(response) {
        alert("Failed");
    });

In this example, we have established a few conventions that the web page and hosting application are expected to follow. Firstly, we only ever pass JSONified strings to the hosting application, because other than scalar values, we cannot really pass objects nicely. Secondly, the hosting application is expected to call a couple of predefined callback functions depending on whether the operation is successful or not. The callback function expects a GUID for a deferred object, and an arbitrary response object. Finally, the window.external methods exposed by the hosting application are expected to return a unique GUID for our web page to use to identify a particular request. For more information on how you should use deferred objects, do check out the documentation on jQuery.

On the hosting application, we can have code that looks like this:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace AsynchronousWebPage
{
    public partial class MainWindow : Form
    {
        public MainWindow()
        {
            InitializeComponent();
            // Browser is a WebBrowser control on the form
            Browser.ObjectForScripting = new JavascriptFunctions(Browser);
        }
    }

    /// <summary>
    /// This class provides the functions available in window.external.
    /// The public methods will be accessible via Javascript
    /// and the class must be COM visible
    /// </summary>
    [ComVisible(true)]
    public class JavascriptFunctions
    {
        private WebBrowser browser;
        private const string SuccessCallback = "externalObject.resolveCallback";
        private const string FailureCallback = "externalObject.rejectCallback";

        public JavascriptFunctions(WebBrowser browser)
        {
            this.browser = browser;
        }

        public string doSomething(string param)
        {
            var deferralGuid = newGuid();
            // Do some long running task
            finishDeferral(deferralGuid, Task.Delay(10000));
            return deferralGuid;
        }

        // Generally async methods returning void should be avoided
        // This is just a demonstration
        // See https://msdn.microsoft.com/en-us/magazine/jj991977.aspx
        private async void finishDeferral(string deferralGuid, Task task)
        {
            try
            {
                await task.ConfigureAwait(false);
                browser.InvokeScript(SuccessCallback, deferralGuid, "Success");
            }
            catch (Exception e)
            {
                // Do something with e
                browser.InvokeScript(FailureCallback, deferralGuid, "Failed");
            }
        }

        private string newGuid()
        {
            return Guid.NewGuid().ToString();
        }
    }
}

The code above adheres to the convention we have set out before, using the Task Parallel Library which contains a powerful set of tools to allow you to program asynchronously without resorting to manual thread management. The await keyword in the finishDeferral method allows the method to yield control back to doSomething for it to return the GUID to its Javascript caller while the task at hand takes a long time to perform its task (in another thread). When the task is done, control is given to finishDeferral to execute the rest of its code. This takes place on the UI thread.

If you do not have access to .NET Framework 4.5, you can always use continuation Tasks to chain tasks one after another. In this case, it might be more painful to deal with exceptions from preceding tasks that have failed. You could try using one of the extension methods I have written for this.

You might have noticed in the WebBrowserExtensions class above that we made use of Control.Invoke method to execute the arbitrary Javascript code. The is especially important if you are stuck using an older version of the .NET Framework. Since we can only call the InvokeScript method on a thread that owns the WebBrowser control, or risk a runtime exception, using the Invoke method ensures that this takes place.

Of course, doing things the other way round (the hosting application calling a long running Javascript function) is also possible if you swap some things around, but you will have to use WebWorkers to avoid locking the web page up. You will also have to ensure that your application will be running on a version of Windows that has a supported version of Internet Explorer installed.

Share

2 Comments

  1. John says:

    Do you know of a way to get the properties/methods in window.external?

  2. Kieran says:

    Window.external is now deprecated. Although this example does still work, it may be worth looking at a better way of achieving these goals for the future now that Microsoft is moving away from IE. Though I’d say that Microsoft will hold on to support for quite some time.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*