40

I have a C# WinForms application that has a WebBrowser control inside of it. I would like to perform two-way communication between my C# form and the JavaScript within the embedded web browser control.

I know I can invoke a JavaScript function with InvokeScript, but how can I invoke C# code from JavaScript in a Document? I guess it wont be easy due to security, but is it possible, somehow, anyhow? These JavaScript functions are supposed to be user functions, pretty much like macros, that would tell the WebBrowser exactly what to do with the help of a whole C# library written by myself. And since this is for a web scraper, JavaScript is the perfect language for these macros since it is pretty much made to access elements in an HTML document.

Gabe
  • 84,912
  • 12
  • 139
  • 238
Juan
  • 15,274
  • 23
  • 105
  • 187
  • Why did I get a minus point for this question? – Juan Sep 12 '10 at 07:00
  • Are you asking how to call a server-side method from client-side code? You'll need to use an AJAX call for that. What, specifically, are you trying to accomplish? – David Sep 12 '10 at 07:02
  • I'm making a web scraper and I would like to be able to read a file in my computer from a JavaScript that is in a html document. Actually I would like to invoke a bunch of methods in my program (the same program that contains the WebBrowser and the Document), but let's say I just want to read a file. – Juan Sep 12 '10 at 07:03
  • @jsoldi: Forgive me, but it sounds like you might be over-engineering this. Why does it need to read an HTML file from the local machine? Can't you just supply the HTML file in a form (uploading it or copying/pasting it into a text box) the easy way? – David Sep 12 '10 at 07:07
  • @jsoldi: you need to explain what your task in general is, because seems like you're trying to solve it in totally wrong way. – zerkms Sep 12 '10 at 07:08
  • Well I also want to call methods to output information to a `DataTable`, save information that stays even when the WebBrowser navigates, etc. – Juan Sep 12 '10 at 07:09
  • @jsoldi: Well, in general, to call server-side code from client-side code you'll need to use AJAX. It's not really something we can just show you, it's more of a concept than a code snippet, so you'll want to research/learn about it. But it still _really_ sounds like this is an unnecessarily difficult approach. – David Sep 12 '10 at 07:15
  • @David, @zerkms: Just edited my question, hope is more clear now. – Juan Sep 12 '10 at 07:19
  • Now the question become even worse :-S You need in AJAX to invoke some server-side logic, regardless the sort of application you are writing. – zerkms Sep 12 '10 at 07:21
  • 1
    I clarified the question so that hopefully nobody still thinks that you're trying to communicate with C# code on a web server. – Gabe Sep 12 '10 at 07:29

3 Answers3

54

What you need to do is set the ObjectForScripting property on the web browser control to an object containing the C# methods you want to call from JavaScript. Then you can access that object from JavaScript using window.external. The only thing to watch out for is that the object has to have the [ComVisibleAttribute(true)] attribute. I've used this successfully for several years.

Here's a page with documenation and a simple example: http://msdn.microsoft.com/en-us/library/a0746166.aspx

Here's the example from the link (I haven't tried this code):

using System;
using System.Windows.Forms;
using System.Security.Permissions;

[PermissionSet(SecurityAction.Demand, Name="FullTrust")]
[System.Runtime.InteropServices.ComVisibleAttribute(true)]
public class Form1 : Form
{
    private WebBrowser webBrowser1 = new WebBrowser();
    private Button button1 = new Button();

    [STAThread]
    public static void Main()
    {
        Application.EnableVisualStyles();
        Application.Run(new Form1());
    }

    public Form1()
    {
        button1.Text = "call script code from client code";
        button1.Dock = DockStyle.Top;
        button1.Click += new EventHandler(button1_Click);
        webBrowser1.Dock = DockStyle.Fill;
        Controls.Add(webBrowser1);
        Controls.Add(button1);
        Load += new EventHandler(Form1_Load);
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        webBrowser1.AllowWebBrowserDrop = false;
        webBrowser1.IsWebBrowserContextMenuEnabled = false;
        webBrowser1.WebBrowserShortcutsEnabled = false;
        webBrowser1.ObjectForScripting = this;
        // Uncomment the following line when you are finished debugging.
        //webBrowser1.ScriptErrorsSuppressed = true;

        webBrowser1.DocumentText =
            "<html><head><script>" +
            "function test(message) { alert(message); }" +
            "</script></head><body><button " +
            "onclick=\"window.external.Test('called from script code')\">" +
            "call client code from script code</button>" +
            "</body></html>";
    }

    public void Test(String message)
    {
        MessageBox.Show(message, "client code");
    }

    private void button1_Click(object sender, EventArgs e)
    {
        webBrowser1.Document.InvokeScript("test",
            new String[] { "called from client code" });
    }
}
Gabe
  • 84,912
  • 12
  • 139
  • 238
  • BTW, I've also used this method for communication between C# in Silverlight code in an embedded web control to the C# form hosting it. – Gabe Sep 12 '10 at 07:31
  • 4
    You should also watch out for that C# method invoked from JavaScript declared as public. – Dmitrii Lobanov Dec 24 '10 at 05:41
  • You shouldn't use the Form object or any other existing object but some dedicated object for that job, because otherwise you could unintentionally allow the javascript to execute some other methods on the object. – NineBerry Sep 07 '14 at 16:19
  • @NineBerry: Yes, that's a good point. You'd want a dedicated class for production code. – Gabe Sep 08 '14 at 01:40
5

You're probably looking for http://msdn.microsoft.com/en-us/library/system.windows.forms.webbrowser.objectforscripting.aspx

WebBrowser.ObjectForScripting lets you expose an instance of a [ComVisible] .net class to javascript code running inside the hosted web browser. It is exposed in javascript as window.external

Excellent article from Microsoft: How to: Implement Two-Way Communication Between DHTML Code and Client Application Code

gpoo
  • 8,408
  • 3
  • 38
  • 53
blucz
  • 1,606
  • 10
  • 13
  • 1
    That article is far from excellent. For example, I'd really like to know whether I can return a value into the javascript code, but that is not at all clear. – Greg Ball Apr 22 '15 at 23:07
2

Here are number of extension methods that I wrote to help with two way communication/invocation between the WebBrowser object and C# code:

using System;
using System.Threading;
using FluentSharp.Web35;
using FluentSharp.WinForms;
using FluentSharp.CoreLib;
using FluentSharp.CoreLib.API;

namespace FluentSharp.Watin
{
    public static class WatiN_IE_ExtensionMethods_Javascript
    {

        public static object invokeScript(this WatiN_IE ie, string functionName)
        {
            return ie.invokeScript(functionName, null);
        }

        public static object invokeScript(this WatiN_IE ie, string functionName, params object[] parameters)
        {
            //"[WatiN_IE] invokeScript '{0}' with parameters:{1}".info(functionName ,parameters.size());
            return ie.invokeScript(true, functionName, parameters);
        }   

        public static object invokeScript(this WatiN_IE ie, bool waitForExecutionComplete, string functionName, params object[] parameters)
        {
            var sync = new AutoResetEvent(false);
            object responseValue = null;
            ie.WebBrowser.invokeOnThread(
                ()=>{
                        var document = ie.WebBrowser.Document;
                        if (parameters.isNull())
                            responseValue = document.InvokeScript(functionName); 
                        else
                            responseValue = document.InvokeScript(functionName, parameters); 
                        sync.Set(); 
                });
            if (waitForExecutionComplete)
                sync.WaitOne();
            return responseValue;   
        }

        public static object invokeEval(this WatiN_IE ie, string evalScript)
        {
            var evalParam = "(function() { " + evalScript + "})();";
            //"[WatiN_IE] invokeEval evalParam: {0}".debug(evalParam);
            return ie.invokeScript("eval", evalParam);   
        }
        public static WatiN_IE.ToCSharp injectJavascriptFunctions(this WatiN_IE ie)
        {
            return ie.injectJavascriptFunctions(false);
        }

        public static WatiN_IE.ToCSharp injectJavascriptFunctions(this WatiN_IE ie, bool resetHooks)
        {
            if (ie.WebBrowser.isNull())
                "in InjectJavascriptFunctions, ie.WebBrowser was null".error();
            else
            {
                if (ie.WebBrowser.ObjectForScripting.isNull() || resetHooks)  
                {
                    ie.WebBrowser.ObjectForScripting = new WatiN_IE.ToCSharp();

                    "Injecting Javascript Hooks * Functions for page: {0}".debug(ie.url());
                    ie.eval("var o2Log = function(message) { window.external.write(message) };");
                    ie.invokeScript("o2Log","Test from Javascript (via toCSharp(message) )");
                    ie.eval("$o2 = window.external");
                    "Injection complete (use o2Log(...) or $o2.write(...)  to talk back to O2".info();
                    return (ie.WebBrowser.ObjectForScripting as WatiN_IE.ToCSharp);
                }
                else 
                {
                    if((ie.WebBrowser.ObjectForScripting is WatiN_IE.ToCSharp))
                        return (ie.WebBrowser.ObjectForScripting as WatiN_IE.ToCSharp);
                    else
                        "in WatiN_IE injectJavascriptFunctions, unexpected type in ie.WebBrowser.ObjectForScripting: {0}".error(ie.WebBrowser.ObjectForScripting.typeName());                   
                }

            }
            return null;
        }

        public static object downloadAndExecJavascriptFile(this WatiN_IE ie, string url)
        {
            "[WatiN_IE] downloadAndExecJavascriptFile: {0}".info(url);
            var javascriptCode = url.uri().getHtml();
            if (javascriptCode.valid())
                ie.eval(javascriptCode);
            return ie;
        }

        public static WatiN_IE injectJavascriptFunctions_onNavigate(this WatiN_IE ie)
        {

            ie.onNavigate((url)=> ie.injectJavascriptFunctions());
            return ie;
        }

        public static WatiN_IE setOnAjaxLog(this WatiN_IE ie, Action<string, string,string,string> onAjaxLog)
        {
            (ie.WebBrowser.ObjectForScripting as WatiN_IE.ToCSharp).OnAjaxLog = onAjaxLog;
            return ie;
        }

        public static WatiN_IE eval_ASync(this WatiN_IE ie, string script)
        {
            O2Thread.mtaThread(()=> ie.eval(script));
            return ie;
        }

        public static WatiN_IE eval(this WatiN_IE ie, string script)
        {
            return ie.eval(script, true);
        }

        public static WatiN_IE eval(this WatiN_IE ie, string script, bool waitForExecutionComplete)
        {
            var executionThread = O2Thread.staThread(()=> ie.IE.RunScript(script));         
            if (waitForExecutionComplete)
                executionThread.Join();
            return ie;  
        }

        public static WatiN_IE alert(this WatiN_IE ie, string alertScript)
        {
            return ie.eval("alert({0});".format(alertScript));
        }

        public static object getJsObject(this WatiN_IE ie)
        {
            var toCSharpProxy = ie.injectJavascriptFunctions();
            if (toCSharpProxy.notNull())
                return toCSharpProxy.getJsObject();
            return null;        
        }

        public static T getJsObject<T>(this WatiN_IE ie, string jsCommand)
        {
            var jsObject = ie.getJsObject(jsCommand);
            if (jsObject is T)
                return (T)jsObject;
            return default(T);
        }

        public static bool doesJsObjectExists(this WatiN_IE ie, string jsCommand)
        {
            var toCSharpProxy = ie.injectJavascriptFunctions();
            if (toCSharpProxy.notNull())
            {
                var command = "window.external.setJsObject(typeof({0}))".format(jsCommand);
                ie.invokeEval(command);
                ie.remapInternalJsObject();             
                return toCSharpProxy.getJsObject().str()!="undefined";
            }
            return false;
        }

        public static object getJsVariable(this WatiN_IE ie, string jsCommand)
        {
            return ie.getJsObject(jsCommand);
        }

        public static object getJsObject(this WatiN_IE ie, string jsCommand)
        {
            var toCSharpProxy = ie.injectJavascriptFunctions();
            if (toCSharpProxy.notNull())
            {
                var command = "window.external.setJsObject({0})".format(jsCommand);
                ie.invokeEval(command);
                ie.remapInternalJsObject();             
                return toCSharpProxy.getJsObject();
            }
            return null;
        }               

        public static WatiN_IE remapInternalJsObject(this WatiN_IE ie)
        {       
            //"setting JS _jsObject variable to getJsObject()".info();
            ie.invokeEval("_jsObject = window.external.getJsObject()"); // creates JS variable to be used from JS
            return ie;
        }

        public static WatiN_IE setJsObject(this WatiN_IE ie, object jsObject)
        {
            var toCSharpProxy = ie.injectJavascriptFunctions();
            if (toCSharpProxy.notNull())            
            {
                toCSharpProxy.setJsObject(jsObject);
                ie.remapInternalJsObject();
            }
            return ie;
        }

        public static object waitForJsObject(this WatiN_IE watinIe)
        {
            return watinIe.waitForJsObject(500, 20);
        }

        public static object waitForJsObject(this WatiN_IE watinIe, int sleepMiliseconds, int maxSleepTimes)
        {                   
            "[WatiN_IE][waitForJsObject] trying to find jsObject for {0} x {1} ms".info(maxSleepTimes, sleepMiliseconds);
            watinIe.setJsObject(null);
            for(var i = 0; i < maxSleepTimes ; i++)
            {
                var jsObject = watinIe.getJsObject();
                if(jsObject.notNull())
                {
                    "[watinIe][waitForJsObject] got value: {0} (n tries)".info(jsObject, i);
                    return jsObject;
                }

                watinIe.sleep(500, false);
            }
            "[WatiN_IE][waitForJsObject] didn't find jsObject after {0} sleeps of {1} ms".error(maxSleepTimes, sleepMiliseconds);
            return null;
        }

        public static object waitForJsVariable(this WatiN_IE watinIe, string jsCommand)
        {
            return watinIe.waitForJsVariable(jsCommand,  500, WatiN_IE_ExtensionMethods.WAITFORJSVARIABLE_MAXSLEEPTIMES);
        }

        public static object waitForJsVariable(this WatiN_IE watinIe, string jsCommand, int sleepMiliseconds, int maxSleepTimes)
        {   
            "[WatiN_IE][waitForJsVariable] trying to find jsObject called '{0}' for {1} x {2} ms".info(jsCommand, maxSleepTimes, sleepMiliseconds);         
            watinIe.setJsObject(null);
            for(var i = 0; i < maxSleepTimes ; i++)
            {
                if (watinIe.doesJsObjectExists(jsCommand))
                {
                    var jsObject = watinIe.getJsObject(jsCommand);
                    "[watinIe][waitForJsVariable] got value: {0} ({1} tries)".info(jsObject, i);
                    return jsObject;
                }                   
                watinIe.sleep(500, false);
            }
            "[WatiN_IE][waitForJsVariable] didn't find jsObject called '{0}' after {1} sleeps of {2} ms".error(jsCommand, maxSleepTimes, sleepMiliseconds);
            return null;
        }

        public static WatiN_IE deleteJsVariable(this WatiN_IE watinIe, string jsVariable)
        {
            var evalString = "try { delete " + jsVariable + " } catch(exception) { }";
            watinIe.eval(evalString);
            return watinIe;
        }


    }
}
Dinis Cruz
  • 4,161
  • 2
  • 31
  • 49