How do I await the result of EvaluateJavascript?

ChristineZuckermanChristineZuckerman USMember ✭✭✭
edited June 2015 in Xamarin.Android

I have a CustomWebViewRenderer for Android that contains an event to process javascript using EvaluateJavascript, and I have a Callback object to catch the result of the javascript, but I need to send that result back up the chain to the initial calling function. Right now OnRunJavascript completes before OnRecieveValue runs, so e.Result is not set properly.

    public void OnRunJavascript(object sender, JavascriptEventArgs e)
    {
        if (Control != null)
        {
                var jsr = new JavascriptResult();
                Control.EvaluateJavascript(string.Format("javascript: {0}", e.Script), jsr);
                e.Result = jsr.Result;
         }
    }

public class JavascriptResult : Java.Lang.Object, IValueCallback
{
    public string Result;
    public void OnReceiveValue(Java.Lang.Object result)
    {
        string json = ((Java.Lang.String)result).ToString();
        Result = Newtonsoft.Json.JsonConvert.DeserializeObject<string>(json);
        Notify();
    }
}

Best Answers

  • ChristineZuckermanChristineZuckerman US ✭✭✭
    Accepted Answer

    I found another post that got me going (though I can't find it now) but what I ended up doing is:

    public class JavascriptResult : Java.Lang.Object, IValueCallback
    {
        private Action<string> _callback;
        public JavascriptResult(Action<string> callback)
        {
            _callback = callback;
        }
    
        public void OnReceiveValue(Java.Lang.Object result)
        {
            _callback?.Invoke(Convert.ToString(result));
        }
    }
    

    then calling it using

        public async Task<string> OnEvaluateJavascript(object sender, JavascriptEventArgs e)
        {
                var reset = new ManualResetEvent(false);
                var response = string.Empty;
                Device.BeginInvokeOnMainThread(() =>
                {
                    Control?.EvaluateJavascript(string.Format("javascript: {0}", e.Script), new JavascriptResult((r) =>
                    {
                        response = r;
                        reset.Set();
                    }));
                });
                await Task.Run(() => { reset.WaitOne(); });
                return response;
    
  • ChristineZuckermanChristineZuckerman US ✭✭✭
    Accepted Answer

    JavascriptEventArgs is a class I created -

    public class JavascriptEventArgs
    {
        public string Function { get; set; }
        public string Script { get; set; }
        public string Result { get; set; }
    }
    

    In my CustomWebView I define the event handler:

        public EventHandler<JavascriptEventArgs> RunJavascript;
    
        /// <summary>
        /// Injects the java script.
        /// </summary>
        /// <param name="script">The script.</param>
        public string InjectJavaScript(JavascriptEventArgs e)
        {
            var handler = RunJavascript;
            handler?.Invoke(this, e);
            return e.Result;
        }
    
        /// <summary>
        /// Calls the js function.
        /// </summary>
        /// <param name="funcName">Name of the function.</param>
        /// <param name="parameters">The parameters.</param>
        public string CallJsFunction(string funcName, params object[] parameters)
        {
            var builder = new StringBuilder();
    
            builder.Append(funcName);
            builder.Append("(");
    
            for (var n = 0; n < parameters.Length; n++)
            {
                builder.Append("\"" + parameters[n] + "\"");
                if (n < parameters.Length - 1)
                {
                    builder.Append(", ");
                }
            }
    
            builder.Append(");");
            var e = new JavascriptEventArgs
            {
                Function = funcName,
                Script = builder.ToString(),
                Result = null
            };
    
            return InjectJavaScript(e);
        }
    

    Then in my CustomWebViewHandler I bind to that event

      private void Bind()
        {
            ((CustomWebView)Element).RunJavascript = OnRunJavascript;
        }
    

Answers

  • EddieConnerEddieConner USMember ✭✭

    I am getting the same results. Did you find a solution?

  • ChristineZuckermanChristineZuckerman USMember ✭✭✭

    No, I haven't. I found 'answers' that said you shouldn't await javascript because you may never return from it, but the same could be said for a HTTP call they are awaitable ..

  • EddieConnerEddieConner USMember ✭✭

    I was not able to get the WebView in Android to return the value as desired. I have switched to the Jint JavaScript Engine and have it working in Xamarin iOS, Xamarin Android, WinRT 8.1 and Windows Desktop with my initial testing.

    https://github.com/sebastienros/jint

  • hvaughanhvaughan USMember ✭✭✭
    edited December 2015

    On iOS I finally had to run an awful while loop which executes a JS function to see if my previous JS function has finished or not. If you would like to see the code for that I can post it.

    On Android, I have been overriding the OnJsAlert function to get results and go from there but for some reason it seems that the OnJsAlert function randomly does not ever get called so I am attempting to do something similar on Android (at least for API 19 and higher). Might have to check out JINT. Thanks for the link!

  • AdeMolajo.9118AdeMolajo.9118 GBMember ✭✭

    I have been querying this for a couple of years. Can anybody at Xamarin let us know if this is possible efficiently?

  • CogwheelCogwheel USMember ✭✭

    You can use TaskCompletionSource in your IValueCallback implementation to make it awaitable, but there are several problems. The biggest is that OnReceiveValue is never called if the JavaScript doesn't return a value. If there are any errors while running the script or if you call something that returns a value sometimes but not others, the await will never succeed.

    More subtly, the the call to EvaluateJavascript and the callback to OnReceiveValue both occur on the main thread. You have to be careful how you await the task in order to avoid deadlocks.

    If you want to give it a go, here's an example:

        private class StringCallback : Java.Lang.Object, IValueCallback
        {
            private TaskCompletionSource<string> source;
    
            public Task<string> Task { get { return source.Task; } }
    
            public StringCallback()
            {
                source = new TaskCompletionSource<string>();
            }
    
            public void OnReceiveValue(Java.Lang.Object value)
            {
                try
                {
                    var jstr = (Java.Lang.String)value;
                    var str = new string(jstr.AsEnumerable().ToArray());
                    source.SetResult(StringUtils.Unquote(str));
                }
                catch (Exception ex)
                {
                    source.SetException(ex);
                }
            }
        }
    

    Then you can do something like

        private void RefreshLocation()
        {
            var callback = new ChapterUrlCallback();
            webView.EvaluateJavascript("window.location.href", callback);
            locationTask = Task.Run(async () =>
            {
                var url = await callback.Task;
                return url;
            });
        }
    
  • ChristineZuckermanChristineZuckerman USMember ✭✭✭

    That did get me further, but I'm not able to await the process.
    My CustomWebView defines a RunJavascript EventHandler:

        public EventHandler<JavascriptEventArgs> RunJavascript;
    

    My CustomWebViewRenderer assigns a function to the handler:

        ((CustomWebView)Element).RunJavascript = OnRunJavascript;
    

    When appropriate, the CustomWebView invokes the handler:

            RunJavascript?.Invoke(this, e);
            return e.Result;                                              //Breakpoint 1
    

    which calls the event:

        public async void OnRunJavascript(object sender, JavascriptEventArgs e)
        {
                    Control.EvaluateJavascript(string.Format("javascript: {0}", e.Script), callback);
                    e.Result = await callback.Task;             //Breakpoint 2
        }
    

    But Breakpoint 1 hits before Breakpoint 2. Since OnRunJavascript hasn't set the result yet, the result of null gets returned up the stack.

    Is there a better way to call into the CustomWebViewRenderer from the CustomWebView?

  • DanielGPDanielGP COMember ✭✭

    I know this post is old, but anyway... I hope it helps

    class JavascriptResult : Java.Lang.Object, IValueCallback
    {
        private TaskCompletionSource<string> source;
        public Task<string> JsResult { get { return source.Task; } }
    
        public JavascriptResult ()
        {
            source = new TaskCompletionSource<string> ();
        }
    
        public void OnReceiveValue (Java.Lang.Object result)
        {
            try {
                string json = ((Java.Lang.String)result).ToString ();
                var rs = Newtonsoft.Json.JsonConvert.DeserializeObject<string> (json);
    
                source.SetResult (rs);
            } catch (Exception ex) {
                source.SetException (ex);
            }
        }
    }
    

    Then call it from an async method so you can await the result

    public async Task EvaluateJs()
    {
        var jsr = new JavascriptResult ();
        var script = "document.getElementById('result').innerHTML;";
    
        webView.EvaluateJavascript (script, jsr);
    
        var result = await jsr.JsResult;
    }
    
  • Felix.xFelix.x USMember ✭✭

    it really helps.

    @DanielGP said:
    I know this post is old, but anyway... I hope it helps

  • ChristineZuckermanChristineZuckerman USMember ✭✭✭

    @DanielGP I finally cycled back around to try this and it's not working for me.
    A breakpoint on the await jsr.JsResult hits before a breakpoint in OnRecieveValue. jsr.JsResult = id=3, Status = WaitingForActivation, Method = {null}
    When OnRecieveValue does hit I am able to see the return value I sent (10) but it doesn't get returned anywhere.
    Do you have a sample project?

  • ChristineZuckermanChristineZuckerman USMember ✭✭✭
    Accepted Answer

    I found another post that got me going (though I can't find it now) but what I ended up doing is:

    public class JavascriptResult : Java.Lang.Object, IValueCallback
    {
        private Action<string> _callback;
        public JavascriptResult(Action<string> callback)
        {
            _callback = callback;
        }
    
        public void OnReceiveValue(Java.Lang.Object result)
        {
            _callback?.Invoke(Convert.ToString(result));
        }
    }
    

    then calling it using

        public async Task<string> OnEvaluateJavascript(object sender, JavascriptEventArgs e)
        {
                var reset = new ManualResetEvent(false);
                var response = string.Empty;
                Device.BeginInvokeOnMainThread(() =>
                {
                    Control?.EvaluateJavascript(string.Format("javascript: {0}", e.Script), new JavascriptResult((r) =>
                    {
                        response = r;
                        reset.Set();
                    }));
                });
                await Task.Run(() => { reset.WaitOne(); });
                return response;
    
  • aidan8181aidan8181 Member

    Hi could some tell me how JavascriptEventArgs is refrenced i'm unable to find any information online on it and i'm unable to add it automatically from the visual studio intelli sense. What dll do I need to add it to my project?

  • ChristineZuckermanChristineZuckerman USMember ✭✭✭
    Accepted Answer

    JavascriptEventArgs is a class I created -

    public class JavascriptEventArgs
    {
        public string Function { get; set; }
        public string Script { get; set; }
        public string Result { get; set; }
    }
    

    In my CustomWebView I define the event handler:

        public EventHandler<JavascriptEventArgs> RunJavascript;
    
        /// <summary>
        /// Injects the java script.
        /// </summary>
        /// <param name="script">The script.</param>
        public string InjectJavaScript(JavascriptEventArgs e)
        {
            var handler = RunJavascript;
            handler?.Invoke(this, e);
            return e.Result;
        }
    
        /// <summary>
        /// Calls the js function.
        /// </summary>
        /// <param name="funcName">Name of the function.</param>
        /// <param name="parameters">The parameters.</param>
        public string CallJsFunction(string funcName, params object[] parameters)
        {
            var builder = new StringBuilder();
    
            builder.Append(funcName);
            builder.Append("(");
    
            for (var n = 0; n < parameters.Length; n++)
            {
                builder.Append("\"" + parameters[n] + "\"");
                if (n < parameters.Length - 1)
                {
                    builder.Append(", ");
                }
            }
    
            builder.Append(");");
            var e = new JavascriptEventArgs
            {
                Function = funcName,
                Script = builder.ToString(),
                Result = null
            };
    
            return InjectJavaScript(e);
        }
    

    Then in my CustomWebViewHandler I bind to that event

      private void Bind()
        {
            ((CustomWebView)Element).RunJavascript = OnRunJavascript;
        }
    
  • aidan8181aidan8181 Member

    Thanks for the help, for my usage this seemed a bit much and I found a way to use WebChromeClient to catch javascript alerts. But I still appreciate the time you have taken to give me a run down of how your code works.

    If anyone is wanting an explanation just let me know. Thanks again Christine :smiley:

  • yourzbuddhayourzbuddha Member ✭✭
    @aidan8181 , how you solved it with webchrome client?
  • yourzbuddhayourzbuddha Member ✭✭
    @ChristineBlanda , how to manage the same for Xamarin.Android native?
Sign In or Register to comment.