Skip to content

Advanced Async JavaScript Binding (JSB)

Alex Maitland edited this page Mar 5, 2021 · 8 revisions

Work In progress, more coming soon!

Before starting here please make sure you've read https://github.com/cefsharp/CefSharp/wiki/General-Usage#async-javascript-binding-jsb as it's important you understand the fundamentals of how to bind an object as they are not covered in this article.

Javascript Callback

  • Can Execute at a later point in time
  • Can return a result
  • Can pass in simply structs/classes as param
  • Takes param array so multiple arguments can be passed

A IJavascriptCallback is a proxy for a method in Javascript. You can call a bound method and pass in a reference to a function, then at some later point in your .Net code you can execute the IJavascriptCallback.ExecuteAsync method and the Javascript function will be called.

public void TestCallback(IJavascriptCallback javascriptCallback)
{
	const int taskDelay = 1500;

	Task.Run(async () =>
	{
		await Task.Delay(taskDelay);

		using (javascriptCallback)
		{
			await javascriptCallback.ExecuteAsync("This callback from C# was delayed " + taskDelay + "ms");
		}
	});
}
function MyCallback(string msg)
{
	alert("My callback returned - " + msg):
}

boundAsync.testCallback(MyCallback);

Javascript Callback with Promise

Starting in version 88.2.90 it's possible to use a call to IJavascriptCallback.ExecuteAsync to resolve a Promise. As CEF doesn't yet support promises we have to hack our own solution, this will require some helper javascript to make the magic happen.

//A helper function that takes a function that returns a `Promise`. You cannot pass in your promise directly as creating a `Promise` immediately
//starts its execution, so you need to pass a function that will be executed when you call `IJavascriptCallback.ExecuteAsync` and the result of the `Promose` resolve/reject will be used as the response for the `IJavascriptCallback` call.

//You should be able to copy an dpaste this to your project, you are welcome to simplify this as it's verbose with comments.
//Improvements welcome.
let convertPromiseToCefSharpCallback = function (p)
{
	let f = function (callbackId, ...args)
	{
		//We immediately return CefSharpDefEvalScriptRes as we will be
		//using a promise and will call sendEvalScriptResponse when our
		//promise has completed
		(async function ()
		{
			try
			{
				//Await the promise
				let response = await p(...args);

				//We're done, let's send our response back to our .Net App
				cefSharp.sendEvalScriptResponse(callbackId, true, response, true);
			}
			catch (err)
			{
				//An error occurred let's send the response back to our .Net App
				cefSharp.sendEvalScriptResponse(callbackId, false, err.message, true);

			}
		})();

		//Let CefSharp know we're going to be defering our response as we have some async/await
		//processing to happen before our callback returns it's value
		return "CefSharpDefEvalScriptRes";
	}

	return f;
}

Using our convertPromiseToCefSharpCallback function above we can pass in our function that creates Promise

//The function convertPromiseToCefSharpCallback can be used in your own projects
//Pass in a function that wraps your promise, not your promise directly
let callback = convertPromiseToCefSharpCallback(function ()
{
	return new Promise((resolve, reject) =>
	{
		setTimeout(() =>
		{
			resolve('Hello from Javascript');
		}, 300);
	});
});

//Call our bound object passing in our callback function
const result= await boundAsync.javascriptCallbackEvalPromise(callback);

In your .Net App you simply need to call the IJavascriptCallback.ExecuteAsync passing in IJavascriptCallback.Id as your first param and then any other params that are required. IMPORTANT the IJavascriptCallback.Id must be first if using the convertPromiseToCefSharpCallback helper function from above.

public async Task<string> JavascriptCallbackEvalPromise(IJavascriptCallback callback)
{
  //Do some processing then execute our callback
  //The Task will be resolved when the Promise has either been resolved or rejected
  var response = await callback.ExecuteAsync(callback.Id, msg);

  //For this very simple example we'll just echo what was passed to us
  return (string)response.Result;
}

Method Parameters

By default passing primitive types is supported when passing parameters to methods. To support passing POCO objects you register your object with BindingOptions.DefaultBinder passed as the options param. Your could would look like:

//For .Net 4.x
browser.JavascriptObjectRepository.Register("mathService", new MathService(), isAsync: true, options: BindingOptions.DefaultBinder);
//For .Net Core/Net 5.0 (only async object registration is supported)
browser.JavascriptObjectRepository.Register("mathService", new MathService(), options: BindingOptions.DefaultBinder);

Supported types include:

  • Primitives (int, string, bool, etc)
  • Arrays of Primitives (int[], string[], bool[], etc)
  • Plain old C# Objects (POCO) (simple request response objects, sub classes are supported)
  • Structs
  • Arrays/Lists/Dictionaries

POCO Param Example

A simple MathService class that performs a multiplication in c#, you can of course do much more complex things, make database calls, etc

//A simple service implemented in C#
public class MathService
{
  public MultiplyResponse Multiple(MultiplyRequest request)
  {
    if (request == null)
    {
      return new MultiplyResponse { Success = false };
    }

    var result = request.Num1 * request.Num2;

    return new MultiplyResponse { Success = true, Result = result };
  }
}

public class MultiplyRequest
{
  public int Num1 { get; set; }
  public int Num2 { get; set; }
}

public class MultiplyResponse
{
  public bool Success { get; set; }
  public int Result { get; set; }
}

//For legacy reasons objects returned from methods aren't converted to camel case, we use the new CamelCaseJavascriptNameConverter
//If you don't want any name conversion then set browser.JavascriptObjectRepository.NameConverter = null
browser.JavascriptObjectRepository.NameConverter = new CefSharp.JavascriptBinding.CamelCaseJavascriptNameConverter();

//For .Net 4.x
browser.JavascriptObjectRepository.Register("mathService", new MathService(), isAsync: true, options: BindingOptions.DefaultBinder);
//For .Net Core/Net 5.0 (only async object registration is supported)
browser.JavascriptObjectRepository.Register("mathService", new MathService(), options: BindingOptions.DefaultBinder);
(async function()
{
  //Bind the mathService object we registered in C#
  await CefSharp.BindObjectAsync("mathService");
  //We now have a mathService object available in the global scope, can also be accessed via window.mathService
  let response = await mathService.multiple(12, 12);
  
  //Without specifying browser.JavascriptObjectRepository.NameConverter = new CefSharp.JavascriptBinding.CamelCaseJavascriptNameConverter(); in C# above
  // you would have response.Result as the property name here.
  var result = response.result;
})();

Exception Handling

TODO: