I'm having trouble adding proper exception handling to existing code that makes heavy use of Silverlight - JavaScript interoperability. In this case, my JavaScript can throw an exception that I want to handle meaningfully in Silverlight.
From Silverlight, I'm creating an instance of a JavaScript object, then later I'm calling a method on that object:
public class MyWrapper
{
dynamic _myJSObject;
public MyWrapper()
{
_myJSObject = HtmlPage.Window.CreateInstance("MyJSObject");
}
public int MyMethod()
{
try
{
int result = (int)_myJSObject.MyMethod();
}
catch (Exception ex)
{
// I want to add meaningful exception handling here
}
}
}
Whenever MyJSObject.MyMethod throws an exception, there are two problems:
The browser shows a message that an exception has occurred.
Information about the exception is not passed to my managed code. Instead I get a RuntimeBinderException which just says "Cannot invoke a non-delegate type" and contains no other information whatsoever. This does not seem to match what is described here; I'd expect an InvalidOperationException.
I've tried avoiding to cast the returned value of the method:
object tmp= _myJSObject.MyMethod();
This makes no difference. Changing the type of exception thrown on the JavaScript side has no effect either.
MyJSObject.prototype.MyMethod = function ()
{
throw "Hello Silverlight!";
}
The only solution I can think of right now is abusing the function's return value to pass information about the exception, but that will make my code a whole lot uglier... so:
Why is the behavior I'm seeing different from what is described in documentation? Does it have to do with my use of dynamic somehow? How can I properly handle exceptions that occur in JavaScript in my managed code?
After quite a bit of experimentation, I concluded that there is no way to directly handle the JavaScript exception from Silverlight. In order to be able to process the exception, the JavaScript code needs to be changed slightly.
Instead of throwing the error, I return it:
function MyMethod()
{
try
{
// Possible exception here
}
catch (ex)
{
return new Error(ex);
}
}
Then on the Silverlight side, I use a wrapper around ScriptObject to turn the return value into an exception again. The key here is the TryInvokeMember method:
public class ScriptObjectWrapper : DynamicObject
{
private ScriptObject _scriptObject;
public ScriptObjectWrapper(ScriptObject scriptObject)
{
_scriptObject = scriptObject;
}
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
result = _scriptObject.Invoke(binder.Name, args);
ScriptObject s = result as ScriptObject;
if (s != null)
{
// The JavaScript Error object defines name and message properties.
string name = s.GetProperty("name") as string;
string message = s.GetProperty("message") as string;
if (name != null && message != null && name.EndsWith("Error"))
{
// Customize this to throw a more specific exception type
// that also exposed the name property.
throw new Exception(message);
}
}
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
try
{
_scriptObject.SetProperty(binder.Name, value);
return true;
}
catch
{
return false;
}
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
try
{
result = _scriptObject.GetProperty(binder.Name);
return true;
}
catch
{
result = null;
return false;
}
}
}
Potentially you could improve this wrapper so it actually injects the JavaScript try-catch mechanism transparently, however in my case I had direct control over the JavaScript source code, so there was no need to do this.
Instead of using the built in JavaScript Error object, it's possible to use your custom objects, as long as the name property ends with Error.
To use the wrapper, the original code would change to:
public MyWrapper()
{
_myJSObject = new ScriptObjectWrapper(
HtmlPage.Window.CreateInstance("MyJSObject"));
}
Related
I am developing an application for Xamarin.UWP which is trying to inject Javascript into a local html file (uri: ms-appdata:///local/index.html) like so:
async void OnWebViewNavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
{
if (args.IsSuccess)
{
// Inject JS script
if (Control != null && Element != null)
{
foreach (var f in Element.RegisteredFunctions.Where(ff => !ff.IsInjected))
{
await Control.InvokeScriptAsync("eval", new[] { string.Format(JavaScriptFunctionTemplate, f.Name) });
f.Injected();
}
}
}
}
Then when the Javascript method is called this will call the OnWebViewScriptNotify method so that I can proccess the request in my application.
The trouble is this doesnt work for some kind of security reasons:
This was a policy decision we made that we have had feedback on so we
re-evaluate it. The same restriction doesn't apply if you use
NavigateToStreamUri together with a resolver object. Internally that's
what happens with ms-appdata:/// anyway.
I then tried what is advised in this case which was to use a resolver as mentioned here: https://stackoverflow.com/a/18979635/2987066
But this has a massive affect on performance, because it is constantly converting all files to a stream to load in, as well as certain pages loading incorrectly.
I then looked at using the AddWebAllowedObject method like so:
private void Control_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
{
if (Control != null && Element != null)
{
foreach (var f in Element.RegisteredFunctions)
{
var communicator = new HtmlCommunicator(f);
Control.AddWebAllowedObject("HtmlCommunicator", communicator);
}
}
}
Where HtmlCommunicator is:
[AllowForWeb]
public sealed class HtmlCommunicator
{
public JSFunctionInjection Function { get; set; }
public HtmlCommunicator(JSFunctionInjection function)
{
Function = function;
}
public void Fred()
{
var d = 2;
//Do something with Function
}
}
and in my html it is like so:
try { window.HtmlCommunicator.Fred(); } catch (err) { }
But this doesn't work either.
So is there a way to work around this rediculous limitation?
So I found this answer: C# class attributes not accessible in Javascript
It says:
I believe you need to define the method name starting with a lower
case character.
For example: change GetIPAddress to getIPAddress.
I tested it on my side and found if I use the upper case name
'GetIPAddress', it won't work. But if I use getIPAddress, it works.
So I tried this:
I created a new project of type Windows Runtime Component as suggested here and I changed my method names to lower case so I had:
[AllowForWeb]
public sealed class HtmlCommunicator
{
public HtmlCommunicator()
{
}
public void fred()
{
var d = 2;
//Do something with Function
}
}
In my javascript I then had:
try { window.HtmlCommunicator.fred(); } catch (err) { }
and in my main UWP project I referenced the new Windows Runtime Component library and had the following:
public HtmlCommunicator communicator { get; set; }
private void Control_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
{
if (Control != null && Element != null)
{
communicator = new HtmlCommunicator();
Control.AddWebAllowedObject("HtmlCommunicator", communicator);
}
}
And this worked!
I have a little problem accessing a QList objects since Javascript. I have a C ++ class that allows me to perform SQL queries since QML / JS. Everything works, I get my results in C ++.
My problem is that I'd returned to QML an QList object.
This is my function in C++ to return SQL result (Note is a simple object with different attributes) :
QList<Note> Storage::setQuery(QString query)
{
QList<Note> noteItems;
QSqlQuery qsqlQuery;
bool ok = qsqlQuery.exec(query);
if(!ok)
{
qDebug() << "Error setQuery" << m_sqlDatabase.lastError();
}
else
{
while (qsqlQuery.next()) {
Note my_note;
QString note = qsqlQuery.value("message").toString();
my_note.setMessage(note);
noteItems.append(my_note);
}
}
return noteItems;
}
But when I call this function from JS I get this error: Unknown method return type: QList<Note>
The problem is the return type, QML JS doesn't know the type QList<Object>, why? What do I do wrong
If you want to use c++ QList as a model in Qml, I would recommend you to use the following procedure. I'm using my own example, you might change it according to your needs.
Storage.h
class Storage : public QObject {
Q_PROPERTY(QQmlListProperty<Note> getList READ getList)
public:
QQmlListProperty<Note> getList();
void setQuery(QString query);
QList<Note> noteItems;;
private:
static void appendList(QQmlListProperty<Note> *property, Note *note);
static Note* cardAt(QQmlListProperty<Note> *property, int index);
static int listSize(QQmlListProperty<Note> *property);
static void clearListPtr(QQmlListProperty<Note> *property);
};
Storage.cpp
void Field::appendList(QQmlListProperty<Card> *property, Note *note) {
Q_UNUSED(property);
Q_UNUSED(note);
}
Note* Field::cardAt(QQmlListProperty<Note> *property, int index) {
return static_cast< QList<Note> *>(property->data)->at(index);
}
int Field::listSize(QQmlListProperty<Note> *property) {
return static_cast< QList<Note> *>(property->data)->size();
}
void Field::clearListPtr(QQmlListProperty<Note> *property) {
return static_cast< QList<Note> *>(property->data)->clear();
}
QQmlListProperty<Note> Field::getList() {
return QQmlListProperty<Note>( this, &list[0], &appendList, &listSize, &cardAt, &clearListPtr );
}
void Storage::setQuery(QString query)
{
QList<Note> noteItems;
QSqlQuery qsqlQuery;
bool ok = qsqlQuery.exec(query);
if(!ok)
{
qDebug() << "Error setQuery" << m_sqlDatabase.lastError();
}
else
{
while (qsqlQuery.next()) {
Note my_note;
QString note = qsqlQuery.value("message").toString();
my_note.setMessage(note);
noteItems.append(my_note);
}
}
}
main.cpp
int main(int argc, char *argv[])
{
qmlRegisterType<Note>();
}
The QQmlListProperty class allows applications to expose list-like properties to QML. To provide a list property, a C++ class must implement the operation callbacks, and then return an appropriate QQmlListProperty value from the property getter. List properties should have no setter. When extending QML with C++ code, a C++ class can be registered with the QML type system to enable the class to be used as a data type within QML code.
Did you register Note as a meta type? Probably that's what's missing:
http://doc.qt.io/qt-5/qmetatype.html#Q_DECLARE_METATYPE
In Qt 4 you'll also have to register QList<Note>, but not in Qt 5.
Oh, and Note should probably be a Q_GADGET, otherwise you cannot access its contents from QML either. Make sure you use Qt 5.5+. Otherwise, you'll need QList<Note*> and make those Note objects inherit from QObject.
There is an existing solution for CefGlue: Call .Net from javascript in CefSharp 1 - wpf
I want exactly this, but for CefGlue: I want to communicate with the App using JavaScript. So when I click a button in my HTML site, I want the application to handle this (for example: start a tcp server).
I tried to register an own CefV8Handler but without success, the Execute function on the handler is never called. Here is what I do right now
protected override void OnWebKitInitialized()
{
Console.WriteLine("Registering testy extension");
Xilium.CefGlue.CefRuntime.RegisterExtension("testy", "var testy;if (!testy)testy = {};(function() {testy.hello = function() {};})();", new V8Handler());
base.OnWebKitInitialized();
}
My V8Handler code looks as follows:
public class V8Handler : Xilium.CefGlue.CefV8Handler
{
protected override bool Execute(string name, CefV8Value obj, CefV8Value[] arguments, out CefV8Value returnValue, out string exception)
{
if (name == "testy")
Console.WriteLine("CALLED TESTY");
else
Console.WriteLine("CALLED SOMETHING WEIRED ({0})", name);
returnValue = CefV8Value.CreateNull();
exception = null;
return true;
}
}
I'm in multiprocess mode, no console window shows "CALLED TESTY" nor "CALLED SOMETHING WEIRED".
Found a solution for that. The trick is to create a CefV8Value (CreateFunction) and assign it to a V8Handler. Then assign this value to the global context. This is what it looks like:
internal class RenderProcessHandler : CefRenderProcessHandler
{
protected override void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context)
{
CefV8Value global = context.GetGlobal();
CefV8Value func = CefV8Value.CreateFunction("magic", new V8Handler());
global.SetValue("magic", func, CefV8PropertyAttribute.None);
base.OnContextCreated(browser, frame, context);
}
}
Another problem came up: it was called in the renderer process, but I required the callback in the browser process. In the CefV8Handlers execute function i did this:
var browser = CefV8Context.GetCurrentContext().GetBrowser();
browser.SendProcessMessage(CefProcessId.Browser, CefProcessMessage.Create("ipc-js." + name));
This way I can retrive the message in the OnProcessMessageReceived function in the CefClient implementation.
You know JavaScript can basically throw any object or even primitive as an exception:
throw 1;
throw { text: "hello" }
Sadly, debuggers like Firefox will log exceptions to console including a link to the code line where the exception was thrown if we throw built-in Error object.
In order to solve that limitation I thought: why don't I override toString and I give an exception instance as argument of Error constructor so exception will be implicitly converted to string?
var ArgumentException = function(args) {
this._argName= args.argName;
}
ArgumentException.prototype = {
_argName: null,
get argName() { return this._argName; },
toString: function() {
return "ArgumentException was thrown. Affected argument: " + this.argName;
}
};
throw Error(new ArgumentException({ argName: "someArgument" }));
Obviously, above code listing is a simplification of a real-world case.
Ok, this works and solve the whole problem.
But this kills the purpose of using exceptions since a try/catch won't be able of handling exceptions by type:
try
{
throw Error(new ArgumentException({ argName: "someArgument" }));
} catch(e) {
if(e instanceof ArgumentException) {
// This will happen never! "e" will hold a Error instance!!!!
}
}
How do you solve this problem? In fact, it's something with Web browser's debugger rather than a problem with actual JavaScript, but as debugging is an important point in any development cycle, it should be took seriously.
Thank you in advance.
EDIT
I want to share my other conclusion:
try
{
debugger;
throw new ArgumentException({ argName: "someArgument" });
} catch(e) {
if(e instanceof ArgumentException) {
}
}
Above solution will log the exception to the debugger console, but it'll stop the debugger before it's ever thrown. Ok, you don't get the link to the exact line but the debugger gets stopped there so you can know where the exception is going to be thrown.
Why not make your exception inherit from Error?
function ArgumentException(data) {
this.name = "ArgumentException";
this.message = arguments.length ? 'Affected argument: ' + data.argName : "Illegal argument";
}
// set up inheritance
ArgumentException.prototype = Object.create(Error.prototype);
ArgumentException.prototype.constructor = ArgumentException;
// use it
try {
throw new ArgumentException({argName: "someArgument"});
} catch(e) {
if(e instanceof ArgumentException) {
console.log(e); // hi there
}
}
// see it normally
throw new ArgumentException({argName: "someOtherArgument"});
// ArgumentException: Affected argument: someOtherArgument
For more, look at Custom Error Types on MDN
Have a problem when try to get a value of a property via Javascript using a NPAPI plugin;
During the debugging I see that all the chain of functions (HasProperty, HasMethod, and GetProperty) are called. More over I see that during calling GetProperty I set the new values into the result parameter. But after leaving GetProperty I get an exception and can't understand what the reason of it.
May FireFox call some additional functions which I forgot to initialize?
Thanks in advance
My code is:
// static function which calls Get_Property for the instance of CScriptableNPObject
bool CScriptableNPObject::NP_GetProperty(NPObject *npobj, NPIdentifier name, NPVariant *result)
{
m_Logs.WriteLogs(10, _T("Enter the CScriptableNPObject::NP_GetProperty()"));
return ((CScriptableNPObject *)npobj)->GetProperty(name, result);
}
// just converter name from NPIdentifier to char *
bool CScriptableNPObject::GetProperty(NPIdentifier name, NPVariant *result)
{
NPUTF8 *pszProperty = m_pNPNFuncs->utf8fromidentifier(name);
return GetProperty(pszProperty, result);
}
// checking the dictionary of properties, if property exists put its value into the result
bool CScriptableNPObject::GetProperty(NPUTF8 *pszProperty, NPVariant *result)
{
VOID_TO_NPVARIANT(*result);
JSPropertiesMap::iterator it = m_JSProperties.find(pszProperty);
if (it == m_JSProperties.end())
return false;
NPUTF8 *pszNewPropertyValue = new NPUTF8[it->second->value.stringValue.UTF8Length + 1];
sprintf(pszNewPropertyValue, it->second->value.stringValue.UTF8Characters);
STRINGZ_TO_NPVARIANT(pszNewPropertyValue, *result);
return true;
}
You need to use NPN_MemAlloc():
NPUTF8* newValue = NPN_MemAlloc(length + 1);