Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Javascript Binding v2 #2246

Closed
amaitland opened this issue Jan 21, 2018 · 29 comments

Comments

@amaitland
Copy link
Member

commented Jan 21, 2018

Due to Chromium changes the current JSB implementation no longer works as it previously did, bound objects are no longer available after cross-site navigation. For details on that see #1203 for historic details.

With the 63.0.0 release changes have been made to revamp the way objects are bound. There will be two options, the legacy behaviour which will still work for Single Page Applications and where only a single domain is used. The new features which will have quite a few advantages.

Legacy Binding

You will need to set CefSharpSettings.LegacyJavascriptBindingEnabled = true before you register your first object (both methods RegisterJsObject and RegisterAsyncJsObject).

//For legacy biding we'll still have support for
CefSharpSettings.LegacyJavascriptBindingEnabled = true;

browser.RegisterJsObject("bound", new BoundObject());
browser.RegisterAsyncJsObject("boundAsync", new BoundAsyncObject());

If you perform cross-site navigation's you will no longer be able to use this method to bind objects. If you only use a single ChromiumWebBrowser instance in your application then it's possible to limit the number of render processes using the renderer-process-limit=1 command line flag. See https://github.com/cefsharp/CefSharp/blob/cefsharp/63/CefSharp.Example/CefExample.cs#L61 for an example.

New Binding Method

The new binding method has many advantages

  • Bind and unbind objects by name
  • Bind a subset of objects to different pages (including popups)
  • Delete/unbind a method
  • Resolve a bound object dynamically

Binding is now initiated by JavaScript, the CefSharp.BindObjectAsync method returns a Promise that is resolved when the objects are available. Objects are created in the global context (window, the same as the previous behaviour). If you call CefSharp.BindObjectAsync without any params then all registered objects will be bound. Binding by name is the more descriptive options.

<script type="text/javascript">
(async function()
{
	// <embed user provided code here>
	await CefSharp.BindObjectAsync("boundAsync", "bound");
	
	boundAsync.hello('CefSharp').then(function (res)
	{
		assert.equal(res, "Hello CefSharp")
		asyncCallback();
	});
})();

(async () =>
{
	await CefSharp.BindObjectAsync("boundAsync", "bound");
	
	boundAsync.hello('CefSharp').then(function (res)
	{
		assert.equal(res, "Hello CefSharp")
		asyncCallback();
	});
})();

CefSharp.BindObjectAsync("boundAsync2").then(function(result)
{
	boundAsync2.hello('CefSharp').then(function (res)
	{
		assert.equal(res, "Hello CefSharp")
                // NOTE the ability to delete a bound object
		assert.equal(true, CefSharp.DeleteBoundObject("boundAsync2"), "Object was unbound");
		assert.ok(window.boundAsync2 === undefined, "boundAsync2 is now undefined");
		asyncCallback();
	});
});
</script>

You have two options for registering an object in .Net, the first is similar to the old behaviour where an object is registered in advance. The second options is more flexible and allows objects to be Resolved when required.

//Firstly you can register an object in a similar fashion to before
//For standard object registration (equivalent to RegisterJsObject)
browser.JavascriptObjectRepository.Register("bound", new BoundObject(), false, options);
//For async object registration (equivalent to RegisterJsObject)
browser.JavascriptObjectRepository.Register("boundAsync", new BoundObject(), true, options);

//Ability to resolve an object, instead of forcing object registration before the browser has been initialized.
browser.JavascriptObjectRepository.ResolveObject += (sender, e) =>
{
	var repo = e.ObjectRepository;
	if (e.ObjectName == "boundAsync2")
	{
		repo.Register("boundAsync2", new AsyncBoundObject(), isAsync: true, options: bindingOptions);
	}
};

To be notified in .Net when objects have been bound in JavaScript then you can subscribe to the ObjectBoundInJavascript event

browser.JavascriptObjectRepository.ObjectBoundInJavascript += (sender, e) =>
{
	var name = e.ObjectName;

	Debug.WriteLine($"Object {e.ObjectName} was bound successfully.");
};    

For reference the set of QUnit test cases used are https://github.com/cefsharp/CefSharp/blob/master/CefSharp.Example/Resources/BindingTest.html

@amaitland amaitland added this to the 63.0.0 milestone Jan 21, 2018

amaitland added a commit to amaitland/CefSharp that referenced this issue Jan 22, 2018

RegisterJsObject and RegisterAsyncJsObject can now only be used when …
…CefSharpSettings.LegacyJavascriptBindingEnabled = true

See cefsharp#2246 for details

amaitland added a commit to amaitland/CefSharp that referenced this issue Jan 22, 2018

RegisterJsObject and RegisterAsyncJsObject can now only be used when …
…CefSharpSettings.LegacyJavascriptBindingEnabled = true

See cefsharp#2246 for details

amaitland added a commit that referenced this issue Jan 22, 2018

Implement Javascript Binding v2 (#2247)
* JSB Rewrite - Add CefSharp.RegisterBoundObject javascript method

To get around the problem of IPC timing, I've reversed the communication, now it's the render process requesting
the bound objects, the implementation is incomplete at this stage, more just a basic proof of concept

Rewrite BindingTest.html using QUnit
One of the tests doesn't map correctly as it requires def user interaction

TODO:

- Get objects by name
- Rename Messages.h values that relate to this change so they are more descriptive
- Better error handling
- Caching of objects within a render process
- Investigate global context options, don't think it's possible to call an async method directly in the global context

# Conflicts:
#	CefSharp.Example/Resources/BindingTest.html

* JSB Improve Request/Response message names

* BindingTest.html - Output stress test results

* Remove JavascriptRootObject was leftover from time we used WCF to transmit objects

* JavascriptObjectRepository - Objects are now sent in the same list with an IsAsync flag

Separation when sending over IPC with two unique lists isn't necessary

* JavascriptObjectRepository - Return objects by name or return all by default

Untabify CefAppUnmanagedWrapper.cpp
Add minor comment to CefBrowserWrapper for a reminder that we use Frame Identifier as dictionary key

* Add //TODO: JSB comments for work that's left to be done

Ideally within a render process we cache bound objects once they've been communicated,
It would also be nice to make multiple binding requests so objects can later be added dynamically

* JSB Allow multiple calls to CefSharp.RegisterBoundObject

Update BindingTest.html to name two distinct calls to different bound objects

* JSB - Add some notes about caching, no working implementation yet

* JSB - Dynamically Register object on Request

* JSB Add ability to unbind an object and rename RegisterBoundObject to BindObjectAsync

# Conflicts:
#	CefSharp.Example/Resources/BindingTest.html

* JSB BindObjectAsync - if objects already bound then return false

* JSB - Ignore Indexer properties

Were previously throwing an exception

* JSB - Add LegacyJavascriptBindingEnabled option so preserve existing behaviour

This is only useful for SPA application and those that only navigate to pages
hosting within a single domain. Any sort of cross-site navigation and a new
render process will be spawned and objects won't be bound automatically
(You can use the new methods to request they're bound yourself, at least in theory,
more testing required on this)

* Add LegacyBindingTest.html

Current disabled by default. To enable uncomment CefSharpSettings.LegacyJavascriptBindingEnabled = true;
in CefExample.cs

* RegisterJsObject and RegisterAsyncJsObject can now only be used when CefSharpSettings.LegacyJavascriptBindingEnabled = true

See #2246 for details

amaitland added a commit that referenced this issue Jan 22, 2018

Implement Javascript Binding v2 (#2247)
* JSB Rewrite - Add CefSharp.RegisterBoundObject javascript method

To get around the problem of IPC timing, I've reversed the communication, now it's the render process requesting
the bound objects, the implementation is incomplete at this stage, more just a basic proof of concept

Rewrite BindingTest.html using QUnit
One of the tests doesn't map correctly as it requires def user interaction

TODO:

- Get objects by name
- Rename Messages.h values that relate to this change so they are more descriptive
- Better error handling
- Caching of objects within a render process
- Investigate global context options, don't think it's possible to call an async method directly in the global context

# Conflicts:
#	CefSharp.Example/Resources/BindingTest.html

* JSB Improve Request/Response message names

* BindingTest.html - Output stress test results

* Remove JavascriptRootObject was leftover from time we used WCF to transmit objects

* JavascriptObjectRepository - Objects are now sent in the same list with an IsAsync flag

Separation when sending over IPC with two unique lists isn't necessary

* JavascriptObjectRepository - Return objects by name or return all by default

Untabify CefAppUnmanagedWrapper.cpp
Add minor comment to CefBrowserWrapper for a reminder that we use Frame Identifier as dictionary key

* Add //TODO: JSB comments for work that's left to be done

Ideally within a render process we cache bound objects once they've been communicated,
It would also be nice to make multiple binding requests so objects can later be added dynamically

* JSB Allow multiple calls to CefSharp.RegisterBoundObject

Update BindingTest.html to name two distinct calls to different bound objects

* JSB - Add some notes about caching, no working implementation yet

* JSB - Dynamically Register object on Request

* JSB Add ability to unbind an object and rename RegisterBoundObject to BindObjectAsync

# Conflicts:
#	CefSharp.Example/Resources/BindingTest.html

* JSB BindObjectAsync - if objects already bound then return false

* JSB - Ignore Indexer properties

Were previously throwing an exception

* JSB - Add LegacyJavascriptBindingEnabled option so preserve existing behaviour

This is only useful for SPA application and those that only navigate to pages
hosting within a single domain. Any sort of cross-site navigation and a new
render process will be spawned and objects won't be bound automatically
(You can use the new methods to request they're bound yourself, at least in theory,
more testing required on this)

* Add LegacyBindingTest.html

Current disabled by default. To enable uncomment CefSharpSettings.LegacyJavascriptBindingEnabled = true;
in CefExample.cs

* RegisterJsObject and RegisterAsyncJsObject can now only be used when CefSharpSettings.LegacyJavascriptBindingEnabled = true

See #2246 for details
@amaitland

This comment has been minimized.

Copy link
Member Author

commented Jan 22, 2018

IJavascriptObjectRepository.ObjectBoundInJavascript event has been added. It's called once per object name that was bound, unclear if it should only be called once and passed in a list of names? Thoughts/comments welcome.

@Example111

This comment has been minimized.

Copy link

commented Jan 23, 2018

I'm doing
browser.JavascriptObjectRepository.Register("boundAsync", new BoundObject(), true, options);

However the method is getting null data. The paramters passed in the js call are null. If I use
then I get the parameters.
browser.JavascriptObjectRepository.Register("boundAsync", new BoundObject(), false, options);

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Jan 23, 2018

@Example111 Can you provide a mode detail example? Are you upgrading an existing app or developing a new one?

browser.JavascriptObjectRepository.Register("boundAsync", new BoundObject(), true, options);

If you set isAsync:true then only methods on your object will be exposed, as there is no way to have an async property.

@Example111

This comment has been minimized.

Copy link

commented Jan 23, 2018

Hi @amaitland sorry I wasn't clear. This is an existing app and was working fine with cross site and the old way to register.
We are trying something like this now this now:

Register:
browser.JavascriptObjectRepository.Register("bound", jsBridge, true, options);

C# function:

public void f(Dictionary<string, object> data = null)
{
  
}

JS:
window.bound.f({a: "1"});

It works but our data parameter is always null. Instead of having the values passed from js

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Jan 23, 2018

@Example111 Thanks for the more detailed example 👍

There was a minor breaking change to add support for dynamic keyword, if you change

public void f(Dictionary<string, object> data = null)

to

public void f(IDictionary<string, object> data = null)

Basically use the interface instead of the implementation and hopefully it'll start working again. Will need to amend the documentation to alert people of the subtle change.

Or alternatively use the dynamic keyword to access the object like

public void f(dynamic data = null)
@chris-araman

This comment has been minimized.

Copy link

commented Jan 25, 2018

We use CEF on Mac and CefSharp on Windows. With the new CefSharp binding implementation, would we need to use Javascript logic like CefSharp.BindObjectAsync(...) on Windows only, while maintaining our exisiting Javascript logic on Mac?

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Jan 26, 2018

@chris-araman You'd be the most qualified to answer your own questions as I've got no idea what your requirements are. Can you use the legacy mode? How many browser instances are you using at any one time? If only one then you can limit the number of render processes.

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Jan 30, 2018

  • Basic caching of binding metadata has been added, so subsequent calls to CefSharp.BindObjectAsync will obtain the metadata from the cache.
  • CefSharp.BindObjectAsync will now return a object with Success, Message, and Count (number of objects bound).

Fixed a bug where Properties weren't being analyzed for sync bound objects.

63.0.0-pre03 will be out shortly.

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Jan 30, 2018

The 63.0.0-pre03 packages should appear on Nuget.org shortly.

The MinimalExample has been updated see https://github.com/cefsharp/CefSharp.MinimalExample/tree/cefsharp/63

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Feb 8, 2018

63.0.0 has been released on Nuget.org

@jsoldi

This comment has been minimized.

Copy link

commented Feb 12, 2018

What would it take to write a CefSharp.GetObject method that, unlike BindObjectAsync, returns the actual object immediately instead of a promise, without adding it to the context? Would this be possible?

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Feb 12, 2018

Issue #2273 has been reported and will be fixed when I'm in front of an actual computer, might be a few days or more.

@jsoldi It would be possible to add a function that returns a promise that resolves to an object without adding it to the context, I had considered this. Promises are required as all communication is done using CEF/Chromium IPC.

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Feb 13, 2018

What would it take to write a CefSharp.GetObject method that, unlike BindObjectAsync, returns the actual object immediately instead of a promise, without adding it to the context? Would this be possible?

For clarity it would be possible to do that if the object has already been cached within a render process, in could in theory be used in conjunction with LegacyBinding. This seems a little limited and I'm not sure if it's actually worth implementing. If you feel like implementing a PR then feel free.

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Feb 13, 2018

Issue #2273 has been reported

The 63.0.1 packages have been pushed to Nuget.org

@fillmorejl

This comment has been minimized.

Copy link

commented Feb 19, 2018

The new binding methodology will not work unless:

CefSharpSettings.LegacyJavascriptBindingEnabled = true;

Is this intended?

The way I am registering is like so:

private ManagedCefBrowserAdapter browserAdapter;

browserAdapter.JavascriptObjectRepository.Register(name, boundObject, true, BindingOptions.DefaultBinder);

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Feb 19, 2018

You must call 'CefSharp.BindObjectAsync' in JavaScript to register an object, see my original post for examples. The behavior of legacy binding is working as expected.

@fillmorejl

This comment has been minimized.

Copy link

commented Feb 19, 2018

Ok, I see now, the extra step of calling CefSharp.BindObjectAsync to get it into the DOM, completely overlooked that part. Thanks!

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Feb 19, 2018

No problem. I'll update the original post with some more details when I'm in front of a computer. Try make it clearer

@omarkelov

This comment has been minimized.

Copy link

commented Feb 24, 2018

Hello! And sorry for my bad english.
I'm working on my first CefSharp app and I need some help.
Shortly, when the page is loaded the program does some usefull actions on the page and calls some methods in my BoundObject via JavaScript. After that it redirects to another url and does exactly the same actions and redirects again and so on...
The only problem is memory leaks. As I figured out, my app leaks if I use object binding in JavaScript. "cefsharp.browsersubprocess.exe" increases memory usage by 30Mb per every redirect. I guess I do some serious mistakes with binding object.
Here is main parts of my code:

public MainWindow() {

	InitializeComponent();

	Browser.JavascriptObjectRepository.Register("boundAsync", new BoundObject(), true);

	Browser.FrameLoadEnd += (sender, args) => {
		IFrame frame = args.Frame;

		if (frame.IsMain) {
			frame.ExecuteJavaScriptAsync(File.ReadAllText("script.js"));
		}
	};
}

script.js looks like:

(async function(){
	await CefSharp.BindObjectAsync("boundAsync");
})();

// do something usefull

boundAsync.callSomeMethod();

window.location.href = "/*href*/";

I'v also tried to unbind object and set it undefined in javascript before redirection:

CefSharp.DeleteBoundObject("boundAsync");
window.boundAsync === undefined;

But it also didn't work. Memory usage continues to increase with every redirect.

I would be very grateful for any kind of help.

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Feb 24, 2018

@CoolPixEll Please fork https://github.com/cefsharp/CefSharp.MinimalExample and provide a working example that reproduces your problem. Then push the changes to GitHub, no zip files thank you.

@lewk2

This comment has been minimized.

Copy link

commented Feb 27, 2018

I'm having a terrible time with the new object binding - I can get trivial binding to work using built-in types, but as soon as I have a method that returns an object, the promise just dies.

I have forked the minimal example, and created a very simple example where an object is bound. This object contains two methods, one returning a simple string and one returning a child object:

namespace CefSharp.MinimalExample.WinForms
{
    public class ExampleBoundObject
    {
        public string SayHello(string name) { return $"Hello {name}!"; }

        public ChildBoundObject GetSubObject() { return new ChildBoundObject(); }
    }

    public class ChildBoundObject
    {
        public string SayGoodbye(string name) { return $"Goodbye {name}!"; }
    }
}

I can bind OK to the top-level object, but calls to the method that returns the ChildBoundObject doesn't work.

I attempted an alternative approach, which was to register the ChildBoundObject also within the JavascriptObjectRepository_ResolveObject method, but then i hit a problem where i could find no good way to update that object (I tried deleting it from JavaScript - but it never updated again.

You can see the fork here:

https://github.com/lewk2/CefSharp.MinimalExample

Having a totally flat object heirarchy is a real pain for me, since I want to walk down an object graph and still be able to access methods as I go. It cannot be that what I'm doing is unsupported - it has to be a bug (I hope, otherwise I must return to IE11 land with the WebBrowser control - not a happy thought!)

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Feb 27, 2018

@lewk2 Thanks for the reproducible example, it is indeed a bug. Short term you can return a struct or an array of classes, it's only returning a single class that's broken. https://github.com/cefsharp/CefSharp/blob/master/CefSharp.Example/AsyncBoundObject.cs#L41

Objects are cached within the render process, deleting an object does not currently remove the object from the meta data cache, I will look to see what's involved in adding a second removeFromCache param.

@lewk2

This comment has been minimized.

Copy link

commented Feb 28, 2018

OK, I did a little more polishing of the test fork (which was actually missing a final git push, because I managed to miss the prompt for my password before leaving for home!).

I can confirm I can get an array of objects returned from a method - but they seem to come back looking more like a flat rendering of the properties of that class. I no longer have any callable methods, and now have accessible properties (which in full async mode should not exist).

I extended the fork to show the general broken case, and also the flattening effect.

I would also like to float the idea that perhaps having even basic type properties (string, int, bool) being able accessible without using promises would be a nice feature. It causes quite a lot of bloat in javascript to access lots of string fields with endless .then calls otherwise...

Regarding the object cache - my plan is to mount just a single proxy bridge class, and work from there. I only started to insert multiple objects as a work-around to my above problems. I could imagine people being blocked in the future if there is no way to remove / replace an object though through the lifecycle of the app... so having a true 'delete' (either in JavaScript or C#) is probably needed.

I now have a difficult choice - we have a project that needs to create a plugin architecture, and I must decide if we fall back to IE11 embedded (which is a total dead-end), move back to CEF legacy binding (which means API / object model we build now within plugins will probably break when we move forward) or to help get the modern bindings working well.

I'm happy to put some effort into 'getting the new stuff working' - but if this is going to take weeks, i need to pick another path...

Anything I can do to get testing or dev effort applied to improving this, I'm happy to pitch in.

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Mar 3, 2018

I can confirm I can get an array of objects returned from a method - but they seem to come back looking more like a flat rendering of the properties of that class. I no longer have any callable methods, and now have accessible properties (which in full async mode should not exist).

Previous versions only supported returning Structs with Fields, support was added for Properties and subsequently the intention was to add support for classes as well. There is no support for accessing a method on an object returned from a method. The returned objects aren't bound. That functionality is complex and would require quite a lot of work. You are welcome to submit a PR.

I would also like to float the idea that perhaps having even basic type properties (string, int, bool) being able accessible without using promises would be a nice feature. It causes quite a lot of bloat in javascript to access lots of string fields with endless .then calls otherwise...

That's not possible with the async implementation. The V8 engine runs in an entirely different process and the async version uses IPC to communicate. You can use the sync version if you require properties.

As for the .then calls, I'd personally use await.

I now have a difficult choice - we have a project that needs to create a plugin architecture, and I must decide if we fall back to IE11 embedded (which is a total dead-end), move back to CEF legacy binding (which means API / object model we build now within plugins will probably break when we move forward) or to help get the modern bindings working well.

Legacy binding is only related to how objects are registered, there are still two options, the sync and async versions. CefGlue and ChromiumFx are both capable CEF wrappers, CefSharp isn't the only option.

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Mar 3, 2018

I should hopefully have time to resolve the final two items related to this issue in the coming week

  • Fix bug when returning class for async binding (commit e47dd3e)
  • Update General Usage Guide to include details on new binding implementation.

I will then finally close #2234

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Mar 8, 2018

The 63.0.2 packages have been pushed to Nuget.org, should appear shortly.

Includes the bug fix for returning a class using async binding.

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Mar 23, 2018

@amaitland amaitland closed this Mar 23, 2018

@amaitland

This comment has been minimized.

Copy link
Member Author

commented Mar 23, 2018

Additional note for those using Legacy Binding and creating multiple ChromiumWebBrowser instances that reference the same domain and have the same object names registered, will need to isolate the browser instances through the use of a ReuqestContext, so they have a unique Render Process, per ChromiumWebBrowser instance, the current implementation. See #2301 for one such case.

There is the potential for this to be a problem with the newer implementation as well, should only be an issue if you bind a different object type using the same key. e.g. browser one has object of type BoundOjbectTest registered as bound and browser two has object of type ObjectTestBind registered as bound. Same solution applies, create the ChromiumWebBrowser instances with a unique RequestContext. Again this should only be a problem if the same domain is used, the browser will attempt to share Render Processes between instances. This will be resolved in #2306.

See RequestContext for more details

@Metalfusion

This comment has been minimized.

Copy link

commented Jul 6, 2019

Seems that CefSharp.DeleteBoundObject(name) doesn't also perform the action of CefSharp.RemoveObjectFromCache(name).

Basically trying to replace a binding like this doesn't work:
browser.JavascriptObjectRepository.Register("myObj", myBoundObj, true, options); // C#
await CefSharp.BindObjectAsync("myObj");
// Using the bound object works here.
await CefSharp.DeleteBoundObject("myObj");
browser.JavascriptObjectRepository.Register("myObj", myBoundObj, true, options); // C#
await CefSharp.BindObjectAsync("myObj");
// Using the object here doesn't work because of the cache.

Adding a call to CefSharp.RemoveObjectFromCache() before DeleteBoundObject() seems to fix the issue, but is unintuitive in my opinion.
In my particular use case I'm also using a dynamic object to which I add delegates and re-register the same instance to the JavascriptObjectRepository, but this shouldn't really affect the cache issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
8 participants
You can’t perform that action at this time.