11

I'm creating an application in C# that hosts custom web pages for most of the GUI. As the host, I'd like to provide a javascript API so that the embedded web pages can access some of the services provided by the host application.

I've been able to get the simple case for this working using the WebBrowser.ObjectForScripting property and implementing a scripting class. This works great for synchronous javascript calls. However, some of the operations that the host provides are long running and I'd like to provide the ability for the javascript to be called back when the operation completes. And this is where I'm running into trouble.

Javascript:

function onComplete( result )
{
    alert( result );
}

function start()
{
    window.external.LongRunningProcess( 'data', onComplete );
}

C#:

[ComVisible(true)]
public class ScriptObject
{
    public void LongRunningProcess( string data, <???> callback )
    {
        // do work, call the callback
    }
}

The 'start' function in javascript kicks this whole process off. The problem I'm having is, What is the type for the callback? And how should I call it from C#?

If I use the string type for callback, it compiles and runs, but from within the LongRunningProcess method callback actually contains the full contents of the onComplete function ( i.e. 'function onComplete( result ) { alert( result ) }' )

If I use the object type, it comes back as a COM object. Using the Microsoft.VisualBasic.Information.TypeName method, it returns 'JScriptTypeInfo'. But as far as I can tell, that's not a real type, nor is there any real mention of it through all of MSDN.

If I use the IReflect interface, it runs without error, but there are no members, fields, or properties on the object that I can find.

A work around would be to pass the string name of the callback function instead of the function itself ( i.e. window.external.LongRunningProcess( 'data', 'onComplete' ); ). I do know how to execute the javascript function by name, but I'd rather not have that syntax be required in the web pages, it also would not work with inline callback definitions in the javascript.

Any Ideas?

For what it's worth, I've already got this system working with the Chromium Embedded framework, but I'm working to port the code over to the WebBrowser control to avoid the hefty size of redistributing Chromium. However, the HTML pages being developed will eventually be run on Linux/Mac OSX where Chromium will probably still be used.

MrSlippers
  • 459
  • 1
  • 6
  • 14
  • i'm not a programmer in C#... and maybe this isn't a solution for your problem, but my collegue use a library to exeguite JS inside C# (i don't remember if C# or .net) maybe is useless for you but you try to see: http://javascriptdotnet.codeplex.com/. Or you can pass `callback` like string `window.external.LongRunningProcess( 'data', 'onComplete');` and in c# you can create a fucntion with this name and perform it like a callback without use JS function in C# (but using a native C# function)... if comment isn't help you i'm sorry for loss time – Frogmouth Jan 15 '14 at 13:56

2 Answers2

17

You can use Reflection for that:

[ComVisible(true)]
public class ScriptObject
{
    public void LongRunningProcess(string data, object callback)
    {
        string result = String.Empty;

        // do work, call the callback

        callback.GetType().InvokeMember(
            name: "[DispID=0]",
            invokeAttr: BindingFlags.Instance | BindingFlags.InvokeMethod,
            binder: null,
            target: callback,
            args: new Object[] { result });
    }
}

You could also try dynamic approach. It'd be more elegant if it works, but I haven't verified it:

[ComVisible(true)]
public class ScriptObject
{
    public void LongRunningProcess(string data, object callback)
    {
        string result = String.Empty;

        // do work, call the callback

        dynamic callbackFunc = callback;
        callbackFunc(result);
    }
}

[UPDATE] The dynamic method indeed works great, and probably is the easiest way of calling back JavaScript from C#, when you have a JavaScript function object. Both Reflection and dynamic allow to call an anonymous JavaScript function, as well. Example:

C#:

public void CallbackTest(object callback)
{
    dynamic callbackFunc = callback;
    callbackFunc("Hello!");
}

JavaScript:

window.external.CallbackTest(function(msg) { alert(msg) })
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    The reflection method worked. Since I'm targeting .NET2.0 I can't use dynamic, but it's good to know that works. How did you know the magic "[DispID=0]" string? Is there documentation for that somewhere? – MrSlippers Jan 16 '14 at 14:15
  • I know it back from the days of COM glory, but I just did a search couldn't find any official source. There are a lot of unofficial ones though, [for example](http://social.msdn.microsoft.com/Forums/ie/en-US/e45d0706-f718-410f-9831-cd487e4cebff/invoking-javascript-method-from-native-code?forum=ieextensiondevelopment). The `DispId[...]` syntax of `Type.InvokeMember` is documented [here](http://msdn.microsoft.com/en-us/library/ekye10tx(v=vs.110).aspx). – noseratio Jan 16 '14 at 19:59
  • 1
    Even better you can avoid the line "dynamic callbackFunc = callback;" by having the parameter of type "dynamic", etc. "dynamic callback". I found some weird issue that it doesn't work if callback doesn't have any parameters, so in your example the "callbackFunc()" wouldn't work. Any idea why? – Martynas Feb 21 '14 at 16:25
  • 3
    @Martynas, indeed `callbackFunc()` doesn't work. I currently have no explanation for this, but there is a very simple workaround, call it like this: `callbackFunc(Type.Missing)`. – noseratio Feb 21 '14 at 20:45
2

As @Frogmouth noted here already you can pass callback function name to the LongRunningProcedure:

function onComplete( result )
{
    alert( result );
}

function start()
{
    window.external.LongRunningProcess( 'data', 'onComplete' );
}

and when LongRunningProcedure completes use .InvokeScript as the following:

    public void LongRunningProcess(string data, string callbackFunctionName)
    {
        // do work, call the callback

        string codeStrig = string.Format("{0}('{1}')", callbackFunctionName, "{{ Your result value here}}");
        webBrowser1.Document.InvokeScript("eval", new [] { codeStrig});  
    }
ShamilS
  • 1,410
  • 2
  • 20
  • 40
  • 1
    `eval` appears to be redundant here. If the callback function is passed by the name (rather than as an object, as in the OP's question), the following would do it without `eval`: `webBrowser1.Document.InvokeScript(callbackFunctionName, new Object [] { result })`. Although, `eval` may be useful when there's a need to inject new JavaScript code into the page ([example](http://stackoverflow.com/a/19002650/1768303)). – noseratio Jan 16 '14 at 01:22