From 44fdfa94c8f8a2f430aee941c944248911df0446 Mon Sep 17 00:00:00 2001 From: Alex Maitland Date: Mon, 22 Jan 2018 10:24:25 +1000 Subject: [PATCH] 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 https://github.com/cefsharp/CefSharp/issues/2246 for details --- .../CefAppUnmanagedWrapper.cpp | 124 +++++++-- .../CefAppUnmanagedWrapper.h | 12 +- .../CefBrowserWrapper.h | 2 +- .../CefSharp.BrowserSubprocess.Core.vcxproj | 2 + ...arp.BrowserSubprocess.Core.vcxproj.filters | 6 + .../JavascriptRootObjectWrapper.cpp | 51 ++-- .../JavascriptRootObjectWrapper.h | 9 +- .../RegisterBoundObjectHandler.h | 163 +++++++++++ .../RegisterBoundObjectRegistry.h | 61 +++++ .../Serialization/JsObjectsSerialization.cpp | 11 +- .../Serialization/JsObjectsSerialization.h | 2 +- CefSharp.Core/CefSettings.h | 3 - CefSharp.Core/Internals/ClientAdapter.cpp | 65 ++++- CefSharp.Core/Internals/Messaging/Messages.h | 4 +- .../Serialization/JsObjectsSerialization.cpp | 9 +- .../Serialization/JsObjectsSerialization.h | 2 +- CefSharp.Core/ManagedCefBrowserAdapter.cpp | 15 -- CefSharp.Core/ManagedCefBrowserAdapter.h | 3 +- CefSharp.Example/BoundObject.cs | 6 + CefSharp.Example/CefExample.cs | 5 + CefSharp.Example/CefSharp.Example.csproj | 1 + .../CefSharpSchemeHandlerFactory.cs | 1 + .../Properties/Resources.Designer.cs | 43 ++- CefSharp.Example/Properties/Resources.resx | 3 + CefSharp.Example/Resources/BindingTest.html | 76 +++++- .../Resources/LegacyBindingTest.html | 253 ++++++++++++++++++ CefSharp.OffScreen/ChromiumWebBrowser.cs | 42 ++- CefSharp.WinForms/ChromiumWebBrowser.cs | 41 ++- CefSharp.Wpf.Example/MainWindow.xaml | 1 + .../Views/BrowserTabView.xaml.cs | 8 + CefSharp.Wpf/ChromiumWebBrowser.cs | 41 ++- CefSharp/CefSharp.csproj | 3 +- CefSharp/CefSharpSettings.cs | 34 ++- CefSharp/Event/JavascriptBindingEventArgs.cs | 22 ++ CefSharp/IJavascriptObjectRepository.cs | 19 ++ CefSharp/IWebBrowser.cs | 2 + CefSharp/Internals/JavascriptObject.cs | 6 + .../Internals/JavascriptObjectRepository.cs | 90 +++++-- CefSharp/Internals/JavascriptRootObject.cs | 21 -- 39 files changed, 1077 insertions(+), 185 deletions(-) create mode 100644 CefSharp.BrowserSubprocess.Core/RegisterBoundObjectHandler.h create mode 100644 CefSharp.BrowserSubprocess.Core/RegisterBoundObjectRegistry.h create mode 100644 CefSharp.Example/Resources/LegacyBindingTest.html create mode 100644 CefSharp/Event/JavascriptBindingEventArgs.cs create mode 100644 CefSharp/IJavascriptObjectRepository.cs delete mode 100644 CefSharp/Internals/JavascriptRootObject.cs diff --git a/CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp b/CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp index c887b872b4..019de027d3 100644 --- a/CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp +++ b/CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp @@ -8,6 +8,7 @@ #include "include\base\cef_logging.h" #include "CefBrowserWrapper.h" #include "CefAppUnmanagedWrapper.h" +#include "RegisterBoundObjectHandler.h" #include "JavascriptRootObjectWrapper.h" #include "Serialization\V8Serialization.h" #include "Serialization\JsObjectsSerialization.h" @@ -72,29 +73,40 @@ namespace CefSharp browser->SendProcessMessage(CefProcessId::PID_BROWSER, contextCreatedMessage); } - auto browserWrapper = FindBrowserWrapper(browser->GetIdentifier(), true); - - auto rootObjectWrappers = browserWrapper->JavascriptRootObjectWrappers; - auto frameId = frame->GetIdentifier(); - - JavascriptRootObjectWrapper^ rootObject; - if (!rootObjectWrappers->TryGetValue(frameId, rootObject)) + if (_legacyBindingEnabled) { - rootObject = gcnew JavascriptRootObjectWrapper(browser->GetIdentifier(), browserWrapper->BrowserProcess); - rootObjectWrappers->TryAdd(frameId, rootObject); - } + auto browserWrapper = FindBrowserWrapper(browser->GetIdentifier(), true); - if (rootObject->IsBound) - { - LOG(WARNING) << "A context has been created for the same browser / frame without context released called previously"; - } - else - { - if (!Object::ReferenceEquals(_javascriptRootObject, nullptr) || !Object::ReferenceEquals(_javascriptAsyncRootObject, nullptr)) + auto rootObjectWrappers = browserWrapper->JavascriptRootObjectWrappers; + auto frameId = frame->GetIdentifier(); + + if (_javascriptObjects->Count > 0) { - rootObject->Bind(_javascriptRootObject, _javascriptAsyncRootObject, context->GetGlobal()); + JavascriptRootObjectWrapper^ rootObject; + if (!rootObjectWrappers->TryGetValue(frameId, rootObject)) + { + rootObject = gcnew JavascriptRootObjectWrapper(browser->GetIdentifier(), browserWrapper->BrowserProcess); + rootObjectWrappers->TryAdd(frameId, rootObject); + } + + rootObject->Bind(_javascriptObjects->Values, context->GetGlobal()); } } + + auto global = context->GetGlobal(); + + auto cefSharpObj = CefV8Value::CreateObject(NULL, NULL); + global->SetValue("CefSharp", cefSharpObj, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_READONLY); + + auto bindObjAsyncFunction = CefV8Value::CreateFunction("BindObjectAsync", new RegisterBoundObjectHandler(_registerBoundObjectRegistry, _javascriptObjects)); + cefSharpObj->SetValue("BindObjectAsync", bindObjAsyncFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE); + + auto unBindObFunction = CefV8Value::CreateFunction("DeleteBoundObject", new RegisterBoundObjectHandler(_registerBoundObjectRegistry, _javascriptObjects)); + cefSharpObj->SetValue("DeleteBoundObject", unBindObFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE); + + //TODO: JSB We could in theory auto bind all the cached objects which would resemble the original JSB behaviour, if + //the cache is empty which would be the case for any cross-site navigation request or the first request made to a browser instance + //then no objects would be bound by default }; void CefAppUnmanagedWrapper::OnContextReleased(CefRefPtr browser, CefRefPtr frame, CefRefPtr context) @@ -190,7 +202,7 @@ namespace CefSharp if (browserWrapper == nullptr) { if (name == kJavascriptCallbackDestroyRequest || - name == kJavascriptRootObjectRequest || + name == kJavascriptRootObjectResponse || name == kJavascriptAsyncMethodCallResponse) { //If we can't find the browser wrapper then we'll just @@ -398,10 +410,78 @@ namespace CefSharp handled = true; } - else if (name == kJavascriptRootObjectRequest) + else if (name == kJavascriptRootObjectResponse) { - _javascriptAsyncRootObject = DeserializeJsRootObject(argList, 0); - _javascriptRootObject = DeserializeJsRootObject(argList, 1); + auto useLegacyBehaviour = argList->GetBool(0); + + //For the old legacy behaviour we add the objects + //to the cache + if (useLegacyBehaviour) + { + _legacyBindingEnabled = true; + + auto javascriptObjects = DeserializeJsObjects(argList, 4); + + for each (JavascriptObject^ obj in Enumerable::OfType(javascriptObjects)) + { + _javascriptObjects->Add(obj->JavascriptName, obj); + } + } + else + { + auto browserId = argList->GetInt(1); + auto frameId = GetInt64(argList, 2); + auto callbackId = GetInt64(argList, 3); + auto javascriptObjects = DeserializeJsObjects(argList, 4); + + //TODO: JSB Implement Caching of JavascriptObjects + //Should caching be configurable? On a per object basis? + /*for each (JavascriptObject^ obj in Enumerable::OfType(javascriptObjects)) + { + if (_javascriptObjects->ContainsKey(obj->JavascriptName)) + { + _javascriptObjects->Remove(obj->JavascriptName); + } + _javascriptObjects->Add(obj->JavascriptName, obj); + }*/ + + auto browserWrapper = FindBrowserWrapper(browser->GetIdentifier(), true); + + auto rootObjectWrappers = browserWrapper->JavascriptRootObjectWrappers; + auto frame = browser->GetFrame(frameId); + if (frame.get()) + { + JavascriptRootObjectWrapper^ rootObject; + if (!rootObjectWrappers->TryGetValue(frameId, rootObject)) + { + rootObject = gcnew JavascriptRootObjectWrapper(browser->GetIdentifier(), browserWrapper->BrowserProcess); + rootObjectWrappers->TryAdd(frameId, rootObject); + } + + auto context = frame->GetV8Context(); + + if (context.get() && context->Enter()) + { + try + { + rootObject->Bind(javascriptObjects, context->GetGlobal()); + + JavascriptAsyncMethodCallback^ callback; + if (_registerBoundObjectRegistry->TryGetAndRemoveMethodCallback(callbackId, callback)) + { + callback->Success(CefV8Value::CreateBool(true)); + + //TODO: JSB deal with failure - no object matching bound + } + } + finally + { + context->Exit(); + } + } + } + } + handled = true; } else if (name == kJavascriptAsyncMethodCallResponse) diff --git a/CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.h b/CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.h index 28d5b2e938..7d61dd7caa 100644 --- a/CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.h +++ b/CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.h @@ -9,6 +9,7 @@ #include "include/cef_base.h" #include "CefBrowserWrapper.h" +#include "RegisterBoundObjectRegistry.h" using namespace System::Collections::Generic; @@ -26,12 +27,12 @@ namespace CefSharp gcroot^> _extensions; gcroot^> _schemes; bool _focusedNodeChangedEnabled; - - // The serialized registered object data waiting to be used (only contains methods and bound async). - gcroot _javascriptAsyncRootObject; + bool _legacyBindingEnabled; // The serialized registered object data waiting to be used. - gcroot _javascriptRootObject; + gcroot^> _javascriptObjects; + + gcroot _registerBoundObjectRegistry; public: static const CefString kPromiseCreatorFunction; @@ -44,6 +45,9 @@ namespace CefSharp _extensions = gcnew List(); _schemes = schemes; _focusedNodeChangedEnabled = enableFocusedNodeChanged; + _javascriptObjects = gcnew Dictionary(); + _registerBoundObjectRegistry = gcnew RegisterBoundObjectRegistry(); + _legacyBindingEnabled = false; } ~CefAppUnmanagedWrapper() diff --git a/CefSharp.BrowserSubprocess.Core/CefBrowserWrapper.h b/CefSharp.BrowserSubprocess.Core/CefBrowserWrapper.h index 2793f299ed..0905e168fd 100644 --- a/CefSharp.BrowserSubprocess.Core/CefBrowserWrapper.h +++ b/CefSharp.BrowserSubprocess.Core/CefBrowserWrapper.h @@ -10,7 +10,6 @@ #include "TypeUtils.h" #include "Stdafx.h" #include "JavascriptRootObjectWrapper.h" -#include "Async/JavascriptAsyncMethodCallback.h" using namespace CefSharp::Internals::Async; using namespace System::ServiceModel; @@ -27,6 +26,7 @@ namespace CefSharp MCefRefPtr _cefBrowser; internal: + //Frame Identifier is used as Key property ConcurrentDictionary^ JavascriptRootObjectWrappers; public: diff --git a/CefSharp.BrowserSubprocess.Core/CefSharp.BrowserSubprocess.Core.vcxproj b/CefSharp.BrowserSubprocess.Core/CefSharp.BrowserSubprocess.Core.vcxproj index c981a8d278..4ad127c6f7 100644 --- a/CefSharp.BrowserSubprocess.Core/CefSharp.BrowserSubprocess.Core.vcxproj +++ b/CefSharp.BrowserSubprocess.Core/CefSharp.BrowserSubprocess.Core.vcxproj @@ -171,6 +171,8 @@ + + diff --git a/CefSharp.BrowserSubprocess.Core/CefSharp.BrowserSubprocess.Core.vcxproj.filters b/CefSharp.BrowserSubprocess.Core/CefSharp.BrowserSubprocess.Core.vcxproj.filters index 881c585fc9..1f397ef535 100644 --- a/CefSharp.BrowserSubprocess.Core/CefSharp.BrowserSubprocess.Core.vcxproj.filters +++ b/CefSharp.BrowserSubprocess.Core/CefSharp.BrowserSubprocess.Core.vcxproj.filters @@ -92,6 +92,12 @@ Header Files + + Header Files + + + Header Files + diff --git a/CefSharp.BrowserSubprocess.Core/JavascriptRootObjectWrapper.cpp b/CefSharp.BrowserSubprocess.Core/JavascriptRootObjectWrapper.cpp index 4f5c6c5e58..3178bb482d 100644 --- a/CefSharp.BrowserSubprocess.Core/JavascriptRootObjectWrapper.cpp +++ b/CefSharp.BrowserSubprocess.Core/JavascriptRootObjectWrapper.cpp @@ -12,38 +12,29 @@ using namespace System::Threading; namespace CefSharp { - void JavascriptRootObjectWrapper::Bind(JavascriptRootObject^ rootObject, JavascriptRootObject^ asyncRootObject, const CefRefPtr& v8Value) + void JavascriptRootObjectWrapper::Bind(ICollection^ objects, const CefRefPtr& v8Value) { - if (_isBound) + if (objects->Count > 0) { - throw gcnew InvalidOperationException("This root object has already been bound."); - } - - _isBound = true; - - if (rootObject != nullptr) - { - auto memberObjects = rootObject->MemberObjects; - for each (JavascriptObject^ obj in Enumerable::OfType(memberObjects)) - { - auto wrapperObject = gcnew JavascriptObjectWrapper(_browserProcess); - wrapperObject->Bind(obj, v8Value, _callbackRegistry); - - _wrappedObjects->Add(wrapperObject); - } - } - - if (asyncRootObject != nullptr) - { - auto memberObjects = asyncRootObject->MemberObjects; auto saveMethod = gcnew Func(this, &JavascriptRootObjectWrapper::SaveMethodCallback); auto promiseCreator = v8Value->GetValue(CefAppUnmanagedWrapper::kPromiseCreatorFunction); - for each (JavascriptObject^ obj in Enumerable::OfType(memberObjects)) - { - auto wrapperObject = gcnew JavascriptAsyncObjectWrapper(_callbackRegistry, saveMethod); - wrapperObject->Bind(obj, v8Value, promiseCreator); - _wrappedAsyncObjects->Add(wrapperObject); + for each (JavascriptObject^ obj in Enumerable::OfType(objects)) + { + if (obj->IsAsync) + { + auto wrapperObject = gcnew JavascriptAsyncObjectWrapper(_callbackRegistry, saveMethod); + wrapperObject->Bind(obj, v8Value, promiseCreator); + + _wrappedAsyncObjects->Add(wrapperObject); + } + else + { + auto wrapperObject = gcnew JavascriptObjectWrapper(_browserProcess); + wrapperObject->Bind(obj, v8Value, _callbackRegistry); + + _wrappedObjects->Add(wrapperObject); + } } } } @@ -53,12 +44,6 @@ namespace CefSharp return _callbackRegistry; } - bool JavascriptRootObjectWrapper::IsBound::get() - { - return _isBound; - } - - int64 JavascriptRootObjectWrapper::SaveMethodCallback(JavascriptAsyncMethodCallback^ callback) { auto callbackId = Interlocked::Increment(_lastCallback); diff --git a/CefSharp.BrowserSubprocess.Core/JavascriptRootObjectWrapper.h b/CefSharp.BrowserSubprocess.Core/JavascriptRootObjectWrapper.h index 474b03ee9b..06da913a67 100644 --- a/CefSharp.BrowserSubprocess.Core/JavascriptRootObjectWrapper.h +++ b/CefSharp.BrowserSubprocess.Core/JavascriptRootObjectWrapper.h @@ -27,7 +27,6 @@ namespace CefSharp initonly List^ _wrappedObjects; initonly List^ _wrappedAsyncObjects; initonly Dictionary^ _methodCallbacks; - bool _isBound; int64 _lastCallback; IBrowserProcess^ _browserProcess; // The entire set of possible JavaScript functions to @@ -42,11 +41,6 @@ namespace CefSharp CefSharp::Internals::JavascriptCallbackRegistry^ get(); } - property bool IsBound - { - bool get(); - } - public: JavascriptRootObjectWrapper(int browserId, IBrowserProcess^ browserProcess) { @@ -55,7 +49,6 @@ namespace CefSharp _wrappedAsyncObjects = gcnew List(); _callbackRegistry = gcnew JavascriptCallbackRegistry(browserId); _methodCallbacks = gcnew Dictionary(); - _isBound = false; } ~JavascriptRootObjectWrapper() @@ -87,7 +80,7 @@ namespace CefSharp bool TryGetAndRemoveMethodCallback(int64 id, JavascriptAsyncMethodCallback^% callback); - void Bind(JavascriptRootObject^ rootObject, JavascriptRootObject^ asyncRootObject, const CefRefPtr& v8Value); + void Bind(ICollection^ objects, const CefRefPtr& v8Value); }; } diff --git a/CefSharp.BrowserSubprocess.Core/RegisterBoundObjectHandler.h b/CefSharp.BrowserSubprocess.Core/RegisterBoundObjectHandler.h new file mode 100644 index 0000000000..660db0df98 --- /dev/null +++ b/CefSharp.BrowserSubprocess.Core/RegisterBoundObjectHandler.h @@ -0,0 +1,163 @@ + +// Copyright � 2010-2017 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +#pragma once + +#include "include/cef_v8.h" +#include "RegisterBoundObjectRegistry.h" +#include "..\CefSharp.Core\Internals\Messaging\Messages.h" +#include "..\CefSharp.Core\Internals\Serialization\Primitives.h" + +using namespace CefSharp::Internals::Messaging; +using namespace CefSharp::Internals::Serialization; + +namespace CefSharp +{ + private class RegisterBoundObjectHandler : public CefV8Handler + { + private: + gcroot _callbackRegistry; + gcroot^> _javascriptObjects; + + public: + RegisterBoundObjectHandler(RegisterBoundObjectRegistry^ callbackRegistery, Dictionary^ javascriptObjects) + { + _callbackRegistry = callbackRegistery; + _javascriptObjects = javascriptObjects; + } + + bool Execute(const CefString& name, CefRefPtr object, const CefV8ValueList& arguments, CefRefPtr& retval, CefString& exception) OVERRIDE + { + auto context = CefV8Context::GetCurrentContext(); + if (context.get()) + { + auto browser = context->GetBrowser(); + + if (context.get() && context->Enter()) + { + try + { + auto global = context->GetGlobal(); + + //TODO: Better name for this function + //TODO: Make this a const + if (name == "DeleteBoundObject") + { + if (arguments.size() == 0 || arguments.size() > 1) + { + //TODO: Improve error message + exception = "Must specify the name of a bound object to unbind, one object at a time."; + + return true; + } + + auto objectName = arguments[0]->GetStringValue(); + + auto global = context->GetGlobal(); + + auto success = global->DeleteValue(objectName); + + retval = CefV8Value::CreateBool(success); + } + else if (name == "BindObjectAsync") + { + auto promiseCreator = global->GetValue(CefAppUnmanagedWrapper::kPromiseCreatorFunction); + + //this will create a promise and give us the reject/resolve functions {p: Promise, res: resolve(), rej: reject()} + auto promiseData = promiseCreator->ExecuteFunctionWithContext(context, nullptr, CefV8ValueList()); + + //return the promose + retval = promiseData->GetValue("p"); + + //References to the promise resolve and reject methods + auto resolve = promiseData->GetValue("res"); + auto reject = promiseData->GetValue("rej"); + + auto callback = gcnew JavascriptAsyncMethodCallback(context, resolve, reject); + + auto callbackId = _callbackRegistry->SaveMethodCallback(callback); + + auto request = CefProcessMessage::Create(kJavascriptRootObjectRequest); + auto argList = request->GetArgumentList(); + auto params = CefListValue::Create(); + + auto boundObjectRequired = false; + + if (arguments.size() > 0) + { + for (auto i = 0; i < arguments.size(); i++) + { + auto objectName = arguments[i]->GetStringValue(); + auto managedObjectName = StringUtils::ToClr(objectName); + + //TODO: JSB Implement Caching of JavascriptObjects + //if (_javascriptObjects->ContainsKey(managedObjectName)) + //{ + // //We have a cached version of the required object so we won't request it again + // //We will still need to sent through a list of cached object names so they can be bound from cache + // //The problem with caching will be that we can only call Promise.Resolve once, so we'll need to gather + // //all our bound data before we return + //} + //else + //{ + + //} + + if (!global->HasValue(objectName)) + { + boundObjectRequired = true; + params->SetString(i, objectName); + } + } + } + else + { + //No objects names were specified so we default to makeing the request + boundObjectRequired = true; + } + + //If objects already exist then we'll just do nothing + if (boundObjectRequired) + { + argList->SetInt(0, browser->GetIdentifier()); + SetInt64(argList, 1, context->GetFrame()->GetIdentifier()); + SetInt64(argList, 2, callbackId); + argList->SetList(3, params); + + browser->SendProcessMessage(CefProcessId::PID_BROWSER, request); + } + else + { + CefV8ValueList returnArgs; + returnArgs.push_back(CefV8Value::CreateBool(false)); + //If all the requested objects are bound then we simply return false + resolve->ExecuteFunctionWithContext(context, nullptr, returnArgs); + } + } + } + finally + { + context->Exit(); + } + } + else + { + exception = "Unable to Enter Context"; + } + } + else + { + exception = "Unable to get current context"; + } + + + return true; + } + + + IMPLEMENT_REFCOUNTING(RegisterBoundObjectHandler) + }; +} + diff --git a/CefSharp.BrowserSubprocess.Core/RegisterBoundObjectRegistry.h b/CefSharp.BrowserSubprocess.Core/RegisterBoundObjectRegistry.h new file mode 100644 index 0000000000..421d91665c --- /dev/null +++ b/CefSharp.BrowserSubprocess.Core/RegisterBoundObjectRegistry.h @@ -0,0 +1,61 @@ +// Copyright � 2010-2017 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +#pragma once + +#include "include/cef_v8.h" + +#include "Async/JavascriptAsyncMethodCallback.h" + +using namespace System::Runtime::Serialization; +using namespace System::Linq; +using namespace System::Threading; +using namespace System::Collections::Generic; +using namespace CefSharp::Internals::Async; + +namespace CefSharp +{ + //TODO: JSB Fix naming of this class, it's pretty horrible currently + private ref class RegisterBoundObjectRegistry + { + private: + initonly Dictionary^ _methodCallbacks; + int64 _lastCallback; + + public: + RegisterBoundObjectRegistry() + { + _methodCallbacks = gcnew Dictionary(); + } + + ~RegisterBoundObjectRegistry() + { + for each(JavascriptAsyncMethodCallback^ var in _methodCallbacks->Values) + { + delete var; + } + _methodCallbacks->Clear(); + } + + int64 SaveMethodCallback(JavascriptAsyncMethodCallback^ callback) + { + auto callbackId = Interlocked::Increment(_lastCallback); + _methodCallbacks->Add(callbackId, callback); + return callbackId; + } + + bool TryGetAndRemoveMethodCallback(int64 id, JavascriptAsyncMethodCallback^% callback) + { + bool result = false; + if (result = _methodCallbacks->TryGetValue(id, callback)) + { + _methodCallbacks->Remove(id); + } + return result; + } + }; +} + + + diff --git a/CefSharp.BrowserSubprocess.Core/Serialization/JsObjectsSerialization.cpp b/CefSharp.BrowserSubprocess.Core/Serialization/JsObjectsSerialization.cpp index 27127306c9..ea4135711a 100644 --- a/CefSharp.BrowserSubprocess.Core/Serialization/JsObjectsSerialization.cpp +++ b/CefSharp.BrowserSubprocess.Core/Serialization/JsObjectsSerialization.cpp @@ -26,8 +26,9 @@ namespace CefSharp jsObject->Id = GetInt64(list, 0); jsObject->Name = StringUtils::ToClr(list->GetString(1)); jsObject->JavascriptName = StringUtils::ToClr(list->GetString(2)); + jsObject->IsAsync = list->GetBool(3); - auto methodList = list->GetList(3); + auto methodList = list->GetList(4); auto methodCount = methodList->GetInt(0); auto k = 1; for (auto j = 0; j < methodCount; j++) @@ -42,7 +43,7 @@ namespace CefSharp jsObject->Methods->Add(jsMethod); } - auto propertyList = list->GetList(4); + auto propertyList = list->GetList(5); auto propertyCount = propertyList->GetInt(0); k = 1; for (auto j = 0; j < propertyCount; j++) @@ -63,13 +64,13 @@ namespace CefSharp return jsObject; } - JavascriptRootObject^ DeserializeJsRootObject(const CefRefPtr& list, int index) + List^ DeserializeJsObjects(const CefRefPtr& list, int index) { - auto result = gcnew JavascriptRootObject(); + auto result = gcnew List(); auto subList = list->GetList(index); for (auto i = 0; i < subList->GetSize(); i++) { - result->MemberObjects->Add(DeserializeJsObject(subList, i)); + result->Add(DeserializeJsObject(subList, i)); } return result; diff --git a/CefSharp.BrowserSubprocess.Core/Serialization/JsObjectsSerialization.h b/CefSharp.BrowserSubprocess.Core/Serialization/JsObjectsSerialization.h index 1d7fe3d9a9..0c9bb2a933 100644 --- a/CefSharp.BrowserSubprocess.Core/Serialization/JsObjectsSerialization.h +++ b/CefSharp.BrowserSubprocess.Core/Serialization/JsObjectsSerialization.h @@ -13,7 +13,7 @@ namespace CefSharp namespace Serialization { JavascriptObject^ DeserializeJsObject(const CefRefPtr& list, int index); - JavascriptRootObject^ DeserializeJsRootObject(const CefRefPtr& list, int index); + List^ DeserializeJsObjects(const CefRefPtr& list, int index); } } } \ No newline at end of file diff --git a/CefSharp.Core/CefSettings.h b/CefSharp.Core/CefSettings.h index c2101b782b..4e62ae525e 100644 --- a/CefSharp.Core/CefSettings.h +++ b/CefSharp.Core/CefSettings.h @@ -41,9 +41,6 @@ namespace CefSharp //Automatically discovered and load a system-wide installation of Pepper Flash. _cefCommandLineArgs->Add("enable-system-flash", "1"); - //Temp workaround for https://github.com/cefsharp/CefSharp/issues/1203 - _cefCommandLineArgs->Add("process-per-tab", "1"); - _focusedNodeChangedEnabled = false; } diff --git a/CefSharp.Core/Internals/ClientAdapter.cpp b/CefSharp.Core/Internals/ClientAdapter.cpp index 65a6db25d1..8aaf94e2af 100644 --- a/CefSharp.Core/Internals/ClientAdapter.cpp +++ b/CefSharp.Core/Internals/ClientAdapter.cpp @@ -569,18 +569,28 @@ namespace CefSharp void ClientAdapter::OnRenderViewReady(CefRefPtr browser) { - if (!Object::ReferenceEquals(_browserAdapter, nullptr) && !_browserAdapter->IsDisposed && !browser->IsPopup()) + if (CefSharpSettings::LegacyJavascriptBindingEnabled) { - auto objectRepository = _browserAdapter->JavascriptObjectRepository; - - if (objectRepository->HasBoundObjects) + if (!Object::ReferenceEquals(_browserAdapter, nullptr) && !_browserAdapter->IsDisposed && !browser->IsPopup()) { - //transmit async bound objects - auto jsRootObjectMessage = CefProcessMessage::Create(kJavascriptRootObjectRequest); - auto argList = jsRootObjectMessage->GetArgumentList(); - SerializeJsObject(objectRepository->AsyncRootObject, argList, 0); - SerializeJsObject(objectRepository->RootObject, argList, 1); - browser->SendProcessMessage(CefProcessId::PID_RENDERER, jsRootObjectMessage); + auto objectRepository = _browserAdapter->JavascriptObjectRepository; + + if (objectRepository->HasBoundObjects) + { + /*auto jsRootObjectMessage = CefProcessMessage::Create(kJavascriptRootObjectRequest); + auto argList = jsRootObjectMessage->GetArgumentList(); + SerializeJsObject(objectRepository->GetObjects(nullptr), argList, 0); + browser->SendProcessMessage(CefProcessId::PID_RENDERER, jsRootObjectMessage);*/ + + auto msg = CefProcessMessage::Create(kJavascriptRootObjectResponse); + auto responseArgList = msg->GetArgumentList(); + responseArgList->SetBool(0, true); //Use Legacy Behaviour (auto bind on context creation) + responseArgList->SetInt(1, -1); //BrowserId + SetInt64(responseArgList, 2, 0); //FrameId + SetInt64(responseArgList, 3, 0); //CallbackId + SerializeJsObjects(objectRepository->GetObjects(nullptr), responseArgList, 4); + browser->SendProcessMessage(CefProcessId::PID_RENDERER, msg); + } } } @@ -1128,7 +1138,40 @@ namespace CefSharp auto argList = message->GetArgumentList(); IJavascriptCallbackFactory^ callbackFactory = _browserAdapter->JavascriptCallbackFactory; - if (name == kOnContextCreatedRequest) + //TODO: Rename messages (remove Root from name) + if (name == kJavascriptRootObjectRequest) + { + if (!Object::ReferenceEquals(_browserAdapter, nullptr) && !_browserAdapter->IsDisposed) + { + auto objectRepository = _browserAdapter->JavascriptObjectRepository; + + if (objectRepository->HasBoundObjects) + { + auto browserId = argList->GetInt(0); + auto frameId = GetInt64(argList, 1); + auto callbackId = GetInt64(argList, 2); + auto objectNames = argList->GetList(3); + + auto names = gcnew List(objectNames->GetSize()); + for (auto i = 0; i < objectNames->GetSize(); i++) + { + names->Add(StringUtils::ToClr(objectNames->GetString(i))); + } + + auto msg = CefProcessMessage::Create(kJavascriptRootObjectResponse); + auto responseArgList = msg->GetArgumentList(); + responseArgList->SetBool(0, false); //Use LegacyBehaviour (false) + responseArgList->SetInt(1, browserId); + SetInt64(responseArgList, 2, frameId); + SetInt64(responseArgList, 3, callbackId); + SerializeJsObjects(objectRepository->GetObjects(names), responseArgList, 4); + browser->SendProcessMessage(CefProcessId::PID_RENDERER, msg); + } + } + + handled = true; + } + else if (name == kOnContextCreatedRequest) { _browserControl->SetCanExecuteJavascriptOnMainFrame(true); diff --git a/CefSharp.Core/Internals/Messaging/Messages.h b/CefSharp.Core/Internals/Messaging/Messages.h index 36847eb745..e3b5ed6e7d 100644 --- a/CefSharp.Core/Internals/Messaging/Messages.h +++ b/CefSharp.Core/Internals/Messaging/Messages.h @@ -24,8 +24,10 @@ namespace CefSharp const CefString kJavascriptCallbackDestroyRequest = "JavascriptCallbackDestroyRequest"; //Message containing the result of a given js function call const CefString kJavascriptCallbackResponse = "JavascriptCallbackDoneResponse"; - //Message containing a js root object for js bindings + //Message containing a request JSB root objects const CefString kJavascriptRootObjectRequest = "JavascriptRootObjectRequest"; + //Message containing the response for the JSB root objects + const CefString kJavascriptRootObjectResponse = "JavascriptRootObjectResponse"; //Message from the render process to request a method invocation on a bound object const CefString kJavascriptAsyncMethodCallRequest = "JavascriptAsyncMethodCallRequest"; //Message from the browser process containing the result of a bound method invocation diff --git a/CefSharp.Core/Internals/Serialization/JsObjectsSerialization.cpp b/CefSharp.Core/Internals/Serialization/JsObjectsSerialization.cpp index b31d8c654f..f0d867b724 100644 --- a/CefSharp.Core/Internals/Serialization/JsObjectsSerialization.cpp +++ b/CefSharp.Core/Internals/Serialization/JsObjectsSerialization.cpp @@ -19,6 +19,7 @@ namespace CefSharp SetInt64(objList, 0, jsObject->Id); objList->SetString(1, StringUtils::ToNative(jsObject->Name)); objList->SetString(2, StringUtils::ToNative(jsObject->JavascriptName)); + objList->SetBool(3, jsObject->IsAsync); auto methodList = CefListValue::Create(); auto j = 0; @@ -30,7 +31,7 @@ namespace CefSharp methodList->SetString(j++, StringUtils::ToNative(jsMethod->JavascriptName)); methodList->SetInt(j++, jsMethod->ParameterCount); } - objList->SetList(3, methodList); + objList->SetList(4, methodList); auto propertyList = CefListValue::Create(); j = 0; @@ -61,16 +62,16 @@ namespace CefSharp propertyList->SetNull(j++); } } - objList->SetList(4, propertyList); + objList->SetList(5, propertyList); list->SetList(index, objList); } - void SerializeJsObject(JavascriptRootObject^ object, const CefRefPtr &list, int index) + void SerializeJsObjects(List^ objects, const CefRefPtr &list, int index) { auto subList = CefListValue::Create(); auto i = 0; - for each (JavascriptObject^ jsObject in object->MemberObjects) + for each (JavascriptObject^ jsObject in objects) { SerializeJsObject(jsObject, subList, i++); } diff --git a/CefSharp.Core/Internals/Serialization/JsObjectsSerialization.h b/CefSharp.Core/Internals/Serialization/JsObjectsSerialization.h index 5e10641e03..119034c677 100644 --- a/CefSharp.Core/Internals/Serialization/JsObjectsSerialization.h +++ b/CefSharp.Core/Internals/Serialization/JsObjectsSerialization.h @@ -14,7 +14,7 @@ namespace CefSharp { namespace Serialization { - void SerializeJsObject(JavascriptRootObject^ object, const CefRefPtr &list, int index); + void SerializeJsObjects(List^ objects, const CefRefPtr &list, int index); } } } \ No newline at end of file diff --git a/CefSharp.Core/ManagedCefBrowserAdapter.cpp b/CefSharp.Core/ManagedCefBrowserAdapter.cpp index e3a4c218eb..24a90f4d50 100644 --- a/CefSharp.Core/ManagedCefBrowserAdapter.cpp +++ b/CefSharp.Core/ManagedCefBrowserAdapter.cpp @@ -98,21 +98,6 @@ void ManagedCefBrowserAdapter::Resize(int width, int height) } } -void ManagedCefBrowserAdapter::RegisterJsObject(String^ name, Object^ object, BindingOptions^ options) -{ - if (!CefSharpSettings::WcfEnabled) - { - throw gcnew InvalidOperationException("To enable synchronous JS bindings set WcfEnabled true in CefSettings during initialization."); - } - - _javaScriptObjectRepository->Register(name, object, options); -} - -void ManagedCefBrowserAdapter::RegisterAsyncJsObject(String^ name, Object^ object, BindingOptions^ options) -{ - _javaScriptObjectRepository->RegisterAsync(name, object, options); -} - IBrowser^ ManagedCefBrowserAdapter::GetBrowser(int browserId) { return _clientAdapter->GetBrowserWrapper(browserId); diff --git a/CefSharp.Core/ManagedCefBrowserAdapter.h b/CefSharp.Core/ManagedCefBrowserAdapter.h index cf4e8de880..63617389de 100644 --- a/CefSharp.Core/ManagedCefBrowserAdapter.h +++ b/CefSharp.Core/ManagedCefBrowserAdapter.h @@ -101,6 +101,7 @@ namespace CefSharp } _webBrowserInternal = nullptr; + delete _javaScriptObjectRepository; _javaScriptObjectRepository = nullptr; } @@ -113,8 +114,6 @@ namespace CefSharp void CreateOffscreenBrowser(IntPtr windowHandle, BrowserSettings^ browserSettings, RequestContext^ requestContext, String^ address); void CreateBrowser(BrowserSettings^ browserSettings, RequestContext^ requestContext, IntPtr sourceHandle, String^ address); virtual void Resize(int width, int height); - void RegisterJsObject(String^ name, Object^ object, BindingOptions^ options); - void RegisterAsyncJsObject(String^ name, Object^ object, BindingOptions^ options); virtual IBrowser^ GetBrowser(int browserId); diff --git a/CefSharp.Example/BoundObject.cs b/CefSharp.Example/BoundObject.cs index 2bb1f08634..82f6c69321 100644 --- a/CefSharp.Example/BoundObject.cs +++ b/CefSharp.Example/BoundObject.cs @@ -20,6 +20,12 @@ public class BoundObject public SubBoundObject SubObject { get; set; } public ExceptionTestBoundObject ExceptionTestObject { get; set; } + public int this[int i] + { + get { return i; } + set { } + } + public uint[] MyUintArray { get { return new uint[] { 7, 8 }; } diff --git a/CefSharp.Example/CefExample.cs b/CefSharp.Example/CefExample.cs index b55f78b593..ff44a7589b 100644 --- a/CefSharp.Example/CefExample.cs +++ b/CefSharp.Example/CefExample.cs @@ -19,6 +19,7 @@ public static class CefExample public const string BaseUrl = "custom://cefsharp"; public const string DefaultUrl = BaseUrl + "/home.html"; public const string BindingTestUrl = BaseUrl + "/BindingTest.html"; + public const string LegacyBindingTestUrl = BaseUrl + "/LegacyBindingTest.html"; public const string PluginsTestUrl = BaseUrl + "/plugins.html"; public const string PopupTestUrl = BaseUrl + "/PopupTest.html"; public const string TooltipTestUrl = BaseUrl + "/TooltipTest.html"; @@ -185,6 +186,10 @@ public static void Init(bool osr, bool multiThreadedMessageLoop, IBrowserProcess //Experimental option where bound async methods are queued on TaskScheduler.Default. //CefSharpSettings.ConcurrentTaskExecution = true; + + //Legacy Binding Behaviour doesn't work for cross-site navigation (navigating to a different domain) + //See issue https://github.com/cefsharp/CefSharp/issues/1203 for details + //CefSharpSettings.LegacyJavascriptBindingEnabled = true; } public static async void RegisterTestResources(IWebBrowser browser) diff --git a/CefSharp.Example/CefSharp.Example.csproj b/CefSharp.Example/CefSharp.Example.csproj index f439caa799..8718ccabbb 100644 --- a/CefSharp.Example/CefSharp.Example.csproj +++ b/CefSharp.Example/CefSharp.Example.csproj @@ -143,6 +143,7 @@ + diff --git a/CefSharp.Example/CefSharpSchemeHandlerFactory.cs b/CefSharp.Example/CefSharpSchemeHandlerFactory.cs index d1e7456f83..185d80e6e4 100644 --- a/CefSharp.Example/CefSharpSchemeHandlerFactory.cs +++ b/CefSharp.Example/CefSharpSchemeHandlerFactory.cs @@ -37,6 +37,7 @@ static CefSharpSchemeHandlerFactory() { "/bootstrap/bootstrap.min.js", Resources.bootstrap_min_js }, { "/BindingTest.html", Resources.BindingTest }, + { "/LegacyBindingTest.html", Resources.LegacyBindingTest }, { "/ExceptionTest.html", Resources.ExceptionTest }, { "/PopupTest.html", Resources.PopupTest }, { "/SchemeTest.html", Resources.SchemeTest }, diff --git a/CefSharp.Example/Properties/Resources.Designer.cs b/CefSharp.Example/Properties/Resources.Designer.cs index 43ff5ede50..b17bbc467f 100644 --- a/CefSharp.Example/Properties/Resources.Designer.cs +++ b/CefSharp.Example/Properties/Resources.Designer.cs @@ -287,18 +287,18 @@ public static System.Drawing.Bitmap beach { ///<html> /// <head> /// <title>Binding Test</title> + /// <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.4.1.css"> /// </head> /// <body> - /// <p> - /// Async Binding Test - /// <span id="asyncresult"></span> - /// <script type="text/javascript"> - /// var asResult = document.getElementById('asyncresult'); - /// - /// function writeAsyncResult(call, end) - /// { - /// var p = document.createElement('p'); - /// [rest of string was truncated]";. + /// <div id="qunit"></div> + /// <div id="qunit-fixture"></div> + /// <script src="https://code.jquery.com/qunit/qunit-2.4.1.js"></script> + /// + /// <!--<script type="text/javascript"> + /// (async function() { + /// // <embed user provided code here> + /// + /// [rest of string was truncated]";. /// public static string BindingTest { get { @@ -510,6 +510,29 @@ public static string home_html { } } + /// + /// Looks up a localized string similar to <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> + ///<html> + /// <head> + /// <title>Legacy Binding Test</title> + /// <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.4.1.css"> + /// </head> + /// <body> + /// <div id="qunit"></div> + /// <div id="qunit-fixture"></div> + /// <script src="https://code.jquery.com/qunit/qunit-2.4.1.js"></script> + /// + /// <script type="text/javascript"> + /// (function() + /// { + /// QUnit.test( "bound.repea [rest of string was truncated]";. + /// + public static string LegacyBindingTest { + get { + return ResourceManager.GetString("LegacyBindingTest", resourceCulture); + } + } + /// /// Looks up a localized string similar to <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> ///<html> diff --git a/CefSharp.Example/Properties/Resources.resx b/CefSharp.Example/Properties/Resources.resx index 6e1b43b8d8..4f9cf8acb1 100644 --- a/CefSharp.Example/Properties/Resources.resx +++ b/CefSharp.Example/Properties/Resources.resx @@ -205,4 +205,7 @@ ..\Resources\UnocodeExampleEqualTo32kb.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + ..\Resources\LegacyBindingTest.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + \ No newline at end of file diff --git a/CefSharp.Example/Resources/BindingTest.html b/CefSharp.Example/Resources/BindingTest.html index 63edd108e6..ad05cad8f1 100644 --- a/CefSharp.Example/Resources/BindingTest.html +++ b/CefSharp.Example/Resources/BindingTest.html @@ -9,9 +9,33 @@
+ + + + + diff --git a/CefSharp.Example/Resources/LegacyBindingTest.html b/CefSharp.Example/Resources/LegacyBindingTest.html new file mode 100644 index 0000000000..894f1e1d08 --- /dev/null +++ b/CefSharp.Example/Resources/LegacyBindingTest.html @@ -0,0 +1,253 @@ + + + + Legacy Binding Test + + + +
+
+ + + + + diff --git a/CefSharp.OffScreen/ChromiumWebBrowser.cs b/CefSharp.OffScreen/ChromiumWebBrowser.cs index 94e723c9f7..5b9e417fe2 100644 --- a/CefSharp.OffScreen/ChromiumWebBrowser.cs +++ b/CefSharp.OffScreen/ChromiumWebBrowser.cs @@ -579,6 +579,15 @@ public void Load(string url) /// called before the underlying CEF browser is created. public void RegisterJsObject(string name, object objectToBind, BindingOptions options = null) { + if (!CefSharpSettings.LegacyJavascriptBindingEnabled) + { + throw new Exception(@"CefSharpSettings.LegacyJavascriptBindingEnabled is curretly false, + for legacy binding you must set CefSharpSettings.LegacyJavascriptBindingEnabled = true + before registering your first object see https://github.com/cefsharp/CefSharp/issues/2246 + for details on the new binding options. If you perform cross-site navigations bound objects will + no longer be registered and you will have to migrate to the new method."); + } + if (IsBrowserInitialized) { throw new Exception("Browser is already initialized. RegisterJsObject must be" + @@ -588,7 +597,14 @@ public void RegisterJsObject(string name, object objectToBind, BindingOptions op //Enable WCF if not already enabled CefSharpSettings.WcfEnabled = true; - managedCefBrowserAdapter.RegisterJsObject(name, objectToBind, options); + var objectRepository = managedCefBrowserAdapter.JavascriptObjectRepository; + + if (objectRepository == null) + { + throw new Exception("Object Repository Null, Browser has likely been Disposed."); + } + + objectRepository.Register(name, objectToBind, false, options); } /// @@ -604,12 +620,34 @@ public void RegisterJsObject(string name, object objectToBind, BindingOptions op /// object will be a standard javascript Promise object which is usable to wait for completion or failure. public void RegisterAsyncJsObject(string name, object objectToBind, BindingOptions options = null) { + if (!CefSharpSettings.LegacyJavascriptBindingEnabled) + { + throw new Exception(@"CefSharpSettings.LegacyJavascriptBindingEnabled is curretly false, + for legacy binding you must set CefSharpSettings.LegacyJavascriptBindingEnabled = true + before registering your first object see https://github.com/cefsharp/CefSharp/issues/2246 + for details on the new binding options. If you perform cross-site navigations bound objects will + no longer be registered and you will have to migrate to the new method."); + } + if (IsBrowserInitialized) { throw new Exception("Browser is already initialized. RegisterJsObject must be" + "called before the underlying CEF browser is created."); } - managedCefBrowserAdapter.RegisterAsyncJsObject(name, objectToBind, options); + + var objectRepository = managedCefBrowserAdapter.JavascriptObjectRepository; + + if (objectRepository == null) + { + throw new Exception("Object Repository Null, Browser has likely been Disposed."); + } + + objectRepository.Register(name, objectToBind, true, options); + } + + public IJavascriptObjectRepository JavascriptObjectRepository + { + get { return managedCefBrowserAdapter == null ? null : managedCefBrowserAdapter.JavascriptObjectRepository; } } /// diff --git a/CefSharp.WinForms/ChromiumWebBrowser.cs b/CefSharp.WinForms/ChromiumWebBrowser.cs index dfffa86b8a..ea794f878c 100644 --- a/CefSharp.WinForms/ChromiumWebBrowser.cs +++ b/CefSharp.WinForms/ChromiumWebBrowser.cs @@ -486,6 +486,15 @@ public void Load(String url) /// called before the underlying CEF browser is created. public void RegisterJsObject(string name, object objectToBind, BindingOptions options = null) { + if (!CefSharpSettings.LegacyJavascriptBindingEnabled) + { + throw new Exception(@"CefSharpSettings.LegacyJavascriptBindingEnabled is curretly false, + for legacy binding you must set CefSharpSettings.LegacyJavascriptBindingEnabled = true + before registering your first object see https://github.com/cefsharp/CefSharp/issues/2246 + for details on the new binding options. If you perform cross-site navigations bound objects will + no longer be registered and you will have to migrate to the new method."); + } + if (IsBrowserInitialized) { throw new Exception("Browser is already initialized. RegisterJsObject must be" + @@ -497,7 +506,14 @@ public void RegisterJsObject(string name, object objectToBind, BindingOptions op //Enable WCF if not already enabled CefSharpSettings.WcfEnabled = true; - managedCefBrowserAdapter.RegisterJsObject(name, objectToBind, options); + var objectRepository = managedCefBrowserAdapter.JavascriptObjectRepository; + + if (objectRepository == null) + { + throw new Exception("Object Repository Null, Browser has likely been Disposed."); + } + + objectRepository.Register(name, objectToBind, false, options); } /// @@ -513,6 +529,15 @@ public void RegisterJsObject(string name, object objectToBind, BindingOptions op /// object will be a standard javascript Promise object which is usable to wait for completion or failure. public void RegisterAsyncJsObject(string name, object objectToBind, BindingOptions options = null) { + if (!CefSharpSettings.LegacyJavascriptBindingEnabled) + { + throw new Exception(@"CefSharpSettings.LegacyJavascriptBindingEnabled is curretly false, + for legacy binding you must set CefSharpSettings.LegacyJavascriptBindingEnabled = true + before registering your first object see https://github.com/cefsharp/CefSharp/issues/2246 + for details on the new binding options. If you perform cross-site navigations bound objects will + no longer be registered and you will have to migrate to the new method."); + } + if (IsBrowserInitialized) { throw new Exception("Browser is already initialized. RegisterJsObject must be" + @@ -521,7 +546,19 @@ public void RegisterAsyncJsObject(string name, object objectToBind, BindingOptio InitializeFieldsAndCefIfRequired(); - managedCefBrowserAdapter.RegisterAsyncJsObject(name, objectToBind, options); + var objectRepository = managedCefBrowserAdapter.JavascriptObjectRepository; + + if (objectRepository == null) + { + throw new Exception("Object Repository Null, Browser has likely been Disposed."); + } + + objectRepository.Register(name, objectToBind, true, options); + } + + public IJavascriptObjectRepository JavascriptObjectRepository + { + get { return managedCefBrowserAdapter == null ? null : managedCefBrowserAdapter.JavascriptObjectRepository; } } /// diff --git a/CefSharp.Wpf.Example/MainWindow.xaml b/CefSharp.Wpf.Example/MainWindow.xaml index 9afc8e8057..6482b0d09b 100644 --- a/CefSharp.Wpf.Example/MainWindow.xaml +++ b/CefSharp.Wpf.Example/MainWindow.xaml @@ -26,6 +26,7 @@ + diff --git a/CefSharp.Wpf.Example/Views/BrowserTabView.xaml.cs b/CefSharp.Wpf.Example/Views/BrowserTabView.xaml.cs index a25d45ff8b..88fe9504aa 100644 --- a/CefSharp.Wpf.Example/Views/BrowserTabView.xaml.cs +++ b/CefSharp.Wpf.Example/Views/BrowserTabView.xaml.cs @@ -32,6 +32,14 @@ public BrowserTabView() MethodInterceptor = new MethodInterceptorLogger() // intercept .net methods calls from js and log it }; browser.RegisterAsyncJsObject("boundAsync", new AsyncBoundObject(), bindingOptions); + browser.JavascriptObjectRepository.ResolveObject += (sender, e) => + { + var repo = e.ObjectRepository; + if (e.ObjectName == "boundAsync2") + { + repo.Register("boundAsync2", new AsyncBoundObject(), isAsync: true, options: bindingOptions); + } + }; browser.DisplayHandler = new DisplayHandler(); browser.LifeSpanHandler = new LifespanHandler(); diff --git a/CefSharp.Wpf/ChromiumWebBrowser.cs b/CefSharp.Wpf/ChromiumWebBrowser.cs index 6bd6ae7c6e..3c816ae6e1 100644 --- a/CefSharp.Wpf/ChromiumWebBrowser.cs +++ b/CefSharp.Wpf/ChromiumWebBrowser.cs @@ -2194,6 +2194,15 @@ public void UseLegacyKeyboardHandler() /// called before the underlying CEF browser is created. public void RegisterJsObject(string name, object objectToBind, BindingOptions options = null) { + if(!CefSharpSettings.LegacyJavascriptBindingEnabled) + { + throw new Exception(@"CefSharpSettings.LegacyJavascriptBindingEnabled is curretly false, + for legacy binding you must set CefSharpSettings.LegacyJavascriptBindingEnabled = true + before registering your first object see https://github.com/cefsharp/CefSharp/issues/2246 + for details on the new binding options. If you perform cross-site navigations bound objects will + no longer be registered and you will have to migrate to the new method."); + } + if (InternalIsBrowserInitialized()) { throw new Exception("Browser is already initialized. RegisterJsObject must be" + @@ -2203,7 +2212,14 @@ public void RegisterJsObject(string name, object objectToBind, BindingOptions op //Enable WCF if not already enabled CefSharpSettings.WcfEnabled = true; - managedCefBrowserAdapter.RegisterJsObject(name, objectToBind, options); + var objectRepository = managedCefBrowserAdapter.JavascriptObjectRepository; + + if (objectRepository == null) + { + throw new Exception("Object Repository Null, Browser has likely been Disposed."); + } + + objectRepository.Register(name, objectToBind, false, options); } /// @@ -2219,12 +2235,33 @@ public void RegisterJsObject(string name, object objectToBind, BindingOptions op /// object will be a standard javascript Promise object which is usable to wait for completion or failure. public void RegisterAsyncJsObject(string name, object objectToBind, BindingOptions options = null) { + if (!CefSharpSettings.LegacyJavascriptBindingEnabled) + { + throw new Exception(@"CefSharpSettings.LegacyJavascriptBindingEnabled is curretly false, + for legacy binding you must set CefSharpSettings.LegacyJavascriptBindingEnabled = true + before registering your first object see https://github.com/cefsharp/CefSharp/issues/2246 + for details on the new binding options. If you perform cross-site navigations bound objects will + no longer be registered and you will have to migrate to the new method."); + } + if (InternalIsBrowserInitialized()) { throw new Exception("Browser is already initialized. RegisterJsObject must be" + "called before the underlying CEF browser is created."); } - managedCefBrowserAdapter.RegisterAsyncJsObject(name, objectToBind, options); + var objectRepository = managedCefBrowserAdapter.JavascriptObjectRepository; + + if (objectRepository == null) + { + throw new Exception("Object Repository Null, Browser has likely been Disposed."); + } + + objectRepository.Register(name, objectToBind, true, options); + } + + public IJavascriptObjectRepository JavascriptObjectRepository + { + get { return managedCefBrowserAdapter == null ? null : managedCefBrowserAdapter.JavascriptObjectRepository; } } /// diff --git a/CefSharp/CefSharp.csproj b/CefSharp/CefSharp.csproj index a7896f873f..50d2f84640 100644 --- a/CefSharp/CefSharp.csproj +++ b/CefSharp/CefSharp.csproj @@ -98,8 +98,10 @@ + + @@ -241,7 +243,6 @@ - diff --git a/CefSharp/CefSharpSettings.cs b/CefSharp/CefSharpSettings.cs index 184a57d6c8..5601b73716 100644 --- a/CefSharp/CefSharpSettings.cs +++ b/CefSharp/CefSharpSettings.cs @@ -17,13 +17,41 @@ public static class CefSharpSettings static CefSharpSettings() { ShutdownOnExit = true; + LegacyJavascriptBindingEnabled = false; WcfTimeout = TimeSpan.FromSeconds(2); } /// - /// WCF is used by JavascriptBinding - /// Disabling effectively disables both of these features. - /// Defaults to true + /// Objects registered using RegisterJsObject and RegisterAsyncJsObject + /// will be automatically bound in the first render process that's created + /// for a ChromiumWebBrowser instance. If you perform a cross-site + /// navigation a process switch will occur and bound objects will no longer + /// be automatically avaliable. For those upgrading from version 57 or below + /// that do no perform cross-site navigation (e.g. Single Page applications or + /// applications that only refer to a single domain) can set this property to + /// true and use the old behaviour.Defaults to false + /// NOTE: Set this before your first call to RegisterJsObject or RegisterAsyncJsObject + /// + /// + /// Javascript binding in CefSharp version 57 and below used the + /// --process-per-tab Process Model to limit the number of render + /// processes to 1 per ChromiumWebBrowser instance, this allowed + /// us to communicate bound javascript objects when the process was + /// initially created (OnRenderViewReady is only called for the first + /// process creation or after a crash), subsiquently all bound objects + /// were registered in ever V8Context in OnContextCreated (executed in the render process). + /// Chromium has made changes and --process-per-tab is not currently working. + /// Performing a cross-site navigation (from one domain to a different domain) + /// will cause a new render process to be created, subsiquent render processes + /// won't have access to the bound object information by default. + /// + public static bool LegacyJavascriptBindingEnabled { get; set; } + + /// + /// WCF is used by RegisterJsObject feature for Javascript Binding + /// It's reccomended that anyone developing a new application use + /// the RegisterAsyncJsObject version which communicates using native + /// Chromium IPC. /// public static bool WcfEnabled { get; set; } diff --git a/CefSharp/Event/JavascriptBindingEventArgs.cs b/CefSharp/Event/JavascriptBindingEventArgs.cs new file mode 100644 index 0000000000..4350389a09 --- /dev/null +++ b/CefSharp/Event/JavascriptBindingEventArgs.cs @@ -0,0 +1,22 @@ +// Copyright © 2010-2017 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +using CefSharp; +using System; +using System.Collections.Generic; + +namespace CefSharp.Event +{ + public class JavascriptBindingEventArgs : EventArgs + { + public IJavascriptObjectRepository ObjectRepository { get; private set; } + public string ObjectName { get; private set; } + + public JavascriptBindingEventArgs(IJavascriptObjectRepository objectRepository, string name) + { + ObjectRepository = objectRepository; + ObjectName = name; + } + } +} diff --git a/CefSharp/IJavascriptObjectRepository.cs b/CefSharp/IJavascriptObjectRepository.cs new file mode 100644 index 0000000000..7f060c6c46 --- /dev/null +++ b/CefSharp/IJavascriptObjectRepository.cs @@ -0,0 +1,19 @@ +// Copyright © 2010-2017 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +using System; + +using CefSharp.Event; +using CefSharp.ModelBinding; + +namespace CefSharp +{ + public interface IJavascriptObjectRepository : IDisposable + { + void Register(string name, object value, bool isAsync, BindingOptions options); + bool HasBoundObjects { get; } + bool IsBound(string name); + event EventHandler ResolveObject; + } +} diff --git a/CefSharp/IWebBrowser.cs b/CefSharp/IWebBrowser.cs index 7c47156ae2..679bc41f12 100644 --- a/CefSharp/IWebBrowser.cs +++ b/CefSharp/IWebBrowser.cs @@ -99,6 +99,8 @@ public interface IWebBrowser : IDisposable /// object will be a standard javascript Promise object which is usable to wait for completion or failure. void RegisterAsyncJsObject(string name, object objectToBind, BindingOptions options = null); + IJavascriptObjectRepository JavascriptObjectRepository { get; } + /// /// Implement and assign to handle dialog events. /// diff --git a/CefSharp/Internals/JavascriptObject.cs b/CefSharp/Internals/JavascriptObject.cs index 86d096a9ac..5e3ac1118d 100644 --- a/CefSharp/Internals/JavascriptObject.cs +++ b/CefSharp/Internals/JavascriptObject.cs @@ -29,6 +29,12 @@ public class JavascriptObject //: DynamicObject maybe later [DataMember] public string JavascriptName { get; set; } + /// + /// Indicate if this object bound as async + /// + [DataMember] + public bool IsAsync { get; set; } + /// /// Indicate if JavascriptName is camel case or not /// diff --git a/CefSharp/Internals/JavascriptObjectRepository.cs b/CefSharp/Internals/JavascriptObjectRepository.cs index f94580475c..775194d0e5 100644 --- a/CefSharp/Internals/JavascriptObjectRepository.cs +++ b/CefSharp/Internals/JavascriptObjectRepository.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Threading; using CefSharp.ModelBinding; +using CefSharp.Event; namespace CefSharp.Internals { @@ -28,10 +29,12 @@ namespace CefSharp.Internals /// All of the registered objects are tracked via meta-data for the objects /// expressed starting with the JavaScriptObject type. /// - public class JavascriptObjectRepository + public class JavascriptObjectRepository : IJavascriptObjectRepository { private static long lastId; + public event EventHandler ResolveObject; + /// /// A hash from assigned object ids to the objects, /// this is done to speed up finding the object in O(1) time @@ -39,26 +42,40 @@ public class JavascriptObjectRepository /// private readonly Dictionary objects = new Dictionary(); - /// - /// This is the root of the objects that get serialized to the child process. - /// - public JavascriptRootObject RootObject { get; private set; } - - /// - /// This is the root of the objects that get serialized to the child - /// process with cef ipc serialization (wcf not required). - /// - public JavascriptRootObject AsyncRootObject { get; private set; } - - public JavascriptObjectRepository() + public void Dispose() { - RootObject = new JavascriptRootObject(); - AsyncRootObject = new JavascriptRootObject(); + ResolveObject = null; } public bool HasBoundObjects { - get { return RootObject.MemberObjects.Count > 0 || AsyncRootObject.MemberObjects.Count > 0; } + get { return objects.Count > 0; } + } + + public bool IsBound(string name) + { + return objects.Values.Any(x => x.Name == name); + } + + public List GetObjects(List names = null) + { + if (names == null || names.Count == 0) + { + //TODO: JSB Declare Constant for All + RaiseResolveObjectEvent("All"); + + return objects.Values.ToList(); + } + + foreach (var name in names) + { + if (!IsBound(name)) + { + RaiseResolveObjectEvent(name); + } + } + + return objects.Values.Where(x => names.Contains(x.JavascriptName)).ToList(); } private JavascriptObject CreateJavascriptObject(bool camelCaseJavascriptNames) @@ -71,29 +88,29 @@ private JavascriptObject CreateJavascriptObject(bool camelCaseJavascriptNames) return result; } - public void RegisterAsync(string name, object value, BindingOptions options) + public void Register(string name, object value, bool isAsync, BindingOptions options) { - AsyncRootObject.MemberObjects.Add(CreateInternal(name, value, analyseProperties: false, options: options)); - } + if (!CefSharpSettings.WcfEnabled && !isAsync) + { + throw new InvalidOperationException("To enable synchronous JS bindings set WcfEnabled true in CefSettings during initialization."); + } - public void Register(string name, object value, BindingOptions options) - { - RootObject.MemberObjects.Add(CreateInternal(name, value, analyseProperties: true, options: options)); - } + //Validation name is unique + if(objects.Values.Count(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)) > 0) + { + throw new ArgumentException("Object already bound with name:" + name , name); + } - private JavascriptObject CreateInternal(string name, object value, bool analyseProperties, BindingOptions options) - { var camelCaseJavascriptNames = options == null ? true : options.CamelCaseJavascriptNames; var jsObject = CreateJavascriptObject(camelCaseJavascriptNames); jsObject.Value = value; jsObject.Name = name; jsObject.JavascriptName = name; + jsObject.IsAsync = isAsync; jsObject.Binder = options == null ? null : options.Binder; jsObject.MethodInterceptor = options == null ? null : options.MethodInterceptor; - AnalyseObjectForBinding(jsObject, analyseMethods: true, analyseProperties: analyseProperties, readPropertyValue: false, camelCaseJavascriptNames: camelCaseJavascriptNames); - - return jsObject; + AnalyseObjectForBinding(jsObject, analyseMethods: true, analyseProperties: isAsync, readPropertyValue: false, camelCaseJavascriptNames: camelCaseJavascriptNames); } public bool TryCallMethod(long objectId, string name, object[] parameters, out object result, out string exception) @@ -329,7 +346,12 @@ private void AnalyseObjectForBinding(JavascriptObject obj, bool analyseMethods, { foreach (var propertyInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => !p.IsSpecialName)) { - if (propertyInfo.PropertyType == typeof (Type) || Attribute.IsDefined(propertyInfo, typeof (JavascriptIgnoreAttribute))) + //https://msdn.microsoft.com/en-us/library/system.reflection.propertyinfo.getindexparameters(v=vs.110).aspx + //An array of type ParameterInfo containing the parameters for the indexes. If the property is not indexed, the array has 0 (zero) elements. + //According to MSDN array has zero elements when it's not an indexer, so in theory no null check is required + var isIndexer = propertyInfo.GetIndexParameters().Length > 0; + var hasIgnoreAttribute = Attribute.IsDefined(propertyInfo, typeof (JavascriptIgnoreAttribute)); + if (propertyInfo.PropertyType == typeof (Type) || isIndexer || hasIgnoreAttribute) { continue; } @@ -354,6 +376,16 @@ private void AnalyseObjectForBinding(JavascriptObject obj, bool analyseMethods, } } + private void RaiseResolveObjectEvent(string name) + { + var handler = ResolveObject; + + if(handler != null) + { + handler(this, new JavascriptBindingEventArgs(this, name)); + } + } + private static JavascriptMethod CreateJavaScriptMethod(MethodInfo methodInfo, bool camelCaseJavascriptNames) { var jsMethod = new JavascriptMethod(); diff --git a/CefSharp/Internals/JavascriptRootObject.cs b/CefSharp/Internals/JavascriptRootObject.cs deleted file mode 100644 index a693ce3707..0000000000 --- a/CefSharp/Internals/JavascriptRootObject.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright © 2010-2017 The CefSharp Authors. All rights reserved. -// -// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. - -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace CefSharp.Internals -{ - [DataContract] - public class JavascriptRootObject - { - [DataMember] - public List MemberObjects { get; set; } - - public JavascriptRootObject() - { - MemberObjects = new List(); - } - } -}