Xamarin Forms WebView Executing Javascript

The existing WebView control has the function to run Javascript on the loaded page, however it doesn’t have the ability to return the value. This post will walk through how to add that functionality in a bindable property.

If you want to see a working sample, have a look at WebViewJavascript.

Extend Control

Extending from the base WebView, we add a Function called EvaluateJavascript, that takes a string, this is the Javascript you want to run, and returns a string, this is the result.

public class WebViewer : WebView
{
    public static BindableProperty EvaluateJavascriptProperty =
    BindableProperty.Create(nameof(EvaluateJavascript), typeof(Func<string, Task<string>>), typeof(WebViewer), null, BindingMode.OneWayToSource);

    public Func<string, Task<string>> EvaluateJavascript
    {
        get { return (Func<string, Task<string>>)GetValue(EvaluateJavascriptProperty); }
        set { SetValue(EvaluateJavascriptProperty, value); }
    }
}

Renderers

We need to add to the custom renderers for each platform, to implement the new function.

Android

[assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))]
namespace Mobile.Droid
{    
    public class WebViewRender : WebViewRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
        {
            base.OnElementChanged(e);

            var webView = e.NewElement as WebViewer;
            if (webView != null)
                webView.EvaluateJavascript = async (js) =>
                {
                    var reset = new ManualResetEvent(false);
                    var response = string.Empty;
                    Device.BeginInvokeOnMainThread(() =>
                    {
                        Control?.EvaluateJavascript(js, new JavascriptCallback((r) => { response = r; reset.Set(); }));
                    });
                    await Task.Run(() => { reset.WaitOne(); });
                    return response;
                };
        }
    }
   
    internal class JavascriptCallback : Java.Lang.Object, IValueCallback
    {
        public JavascriptCallback(Action<string> callback)
        {
            _callback = callback;
        }

        private Action<string> _callback;
        public void OnReceiveValue(Java.Lang.Object value)
        {
            _callback?.Invoke(Convert.ToString(value));
        }
    }
}

iOS

[assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))]
namespace Mobile.iOS
{
    public class WebViewRender : WebViewRenderer
    {
        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            var webView = e.NewElement as WebViewer; 
            if (webView != null)
                webView.EvaluateJavascript = (js) =>
                {
                    return Task.FromResult(this.EvaluateJavascript(js));
                };
        }
    }
}

UWP

[assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))]
namespace Mobile.UWP.CustomRenderers
{
    public class WebViewRender : WebViewRenderer
    {
        protected async override void OnElementChanged(ElementChangedEventArgs<WebView> e)
        {
            base.OnElementChanged(e);
            var webView = e.NewElement as WebViewer;
            if (webView != null)
                webView.EvaluateJavascript = async (js) =>
                {
                    return await Control.InvokeScriptAsync("eval", new[] { js });
                };
        }
    }
}

Calling From View Model

First we need to create a property in our View Model, as below.

private Func<string, Task<string>> _evaluateJavascript;
public Func<string, Task<string>> EvaluateJavascript
{
    get { return _evaluateJavascript; }
    set { _evaluateJavascript = value; }
}

Next, we will bind the property from the control to the View Model.

<control:WebViewer Source="{Binding WebViewSource}"
                   EvaluateJavascript="{Binding EvaluateJavascript}, Mode=OneWayToSource}" />

Now we can call our function from our View Model and retrieve the result.

var result = await EvaluateJavascript("document.getElementById('test');");

Warnings

There is a big pitfall you might face, running Javascript that you might need to take note of.

Android

In Android, on versions 4.1 and below you can easily use this Javascript and return a result.

document.getElementById('myElement').value;

However on 4.2+ the Javascript engine changes, hence when you run the above command, it initially returns the correct result, however it also then sets the entire document object in the DOM to the result of that script. Now, if you call the function above again, your script won’t be able to find the element because it no longer exists, your entire DOM is the past result.

To work around this issue, you must assign the result of your script to a variable. You don’t need to return the variable, just set it to a variable and the value will be returned, without affecting the DOM of the existing page.

var x = document.getElementById('myElement').value;
Microsoft MVP | Xamarin MVP | Xamarin Certified Developer |
Exrin MVVM Framework | Xamarin Forms Developer | Melbourne, Australia

Related Posts

18 Comments

  1. Teodor

    First of all – I just found your site and it’s amazing! Thanks for Your effort! You just got subscriber 🙂 I just failed in my attempts to run HybridWebView just to return value from JavaScript and here’s Your nice solution to all my problems!

    But could You show a bit more code? Like whole files? Or provide sample via GitHub?

    I tried to fallow Your solution but failed on:
    var webView = e.NewElement as WebView;
    in Android Renderer
    with error: The type or namespace name ‘WebView’ could not be found (are you missing a using directive or an assembly reference?)
    and
    get { return (Func<string, Task>)GetValue(EvaluateJavascriptProperty); }
    in WebVier class
    with error:
    Cannot implicitly convert type ‘System.Func<string, System.Threading.Tasks.Task>’ to ‘System.Action’

    I know it’s probably my newbie fault like not adding proper using but it still stops me :<

    1. Adam Pedley

      Thanks Tedor. Apologies, those were all typos on my part. I have fixed the code up above.

      It should be ‘as WebViewer’

      and the type on the property needed to be changed from Action to Func>.

      Let me know if you run into any more issues.

  2. Unixir

    Thank you Adam. Your post is awesome!!

    I wish to hook Javascript return string value such as document.getElementsByTagName(‘p’)).

    I tried your code. but “Calling From View Model” section is dificult for me… I don’t understand MVVM very well . How do I express ViewModel logic ?

    1. Adam Pedley

      In MVVM there is a View and ViewModel that are bound together via the BindingContext. While they are some great frameworks out there to help with this, I will keep it simple here.

      In your View on your Constructor you would go

      BindingContext = new MyViewModel();

      This means any binding will try to bind to a property in that class.

      In your MyViewModel, add in the property of EvaluateJavascript.

      In your View you add in the custom WebView control and you will see it binds to this property. This is how the View and ViewModel will link together.

      Then in your MyViewModel, you can just call the EvaluateJavascript function anywhere, in order to execute your JS code and return the result.

  3. Justin Emmanuel

    Hi, thanks for your article. I’m also having a bit of a strange issue.
    I am using HtmlWebViewSource to pull in a local file to populate a string.
    It all seems to be working…. That is up until the point where I call a javascript function.
    If I call something that returns 2+2, no problem I get the result 4.
    However, if I call anything on the DOM document.getElementById for example, or even $(“*”).
    I get nothing.
    I can’t return anything from within the in-memory HTML page.
    Do you know what’s going on?
    I’m running it on the iOS simulator.

  4. Ara Cakaroglu

    Hi Adam , is it possible to see your working code somewhere?
    I did Extend Control and added WebViewRendere but mixed up after that.

    thank you.

  5. Ara Cakaroglu

    I am trying to make mobile 3D Bank Payment with Xamarin.

    With WebView I post the request, Bank is returning a page to enter validation.

    untill here no problem, After Entering validation bank is returning an url for web applications. For web Applications we can get Response parameters with form[“variablename”].

    But I couldnt handle this with xamarin.

    this example helps?
    thank you

  6. Patrick Brady

    Adam, thank you for the start on the code, I understand the concepts behind it but I am still very confused on the implementation. Like the others have said, if you could post the code of a working solution that would be immensely helpful.

    I am trying to get a string back from a javascript call to a webpage but when I try I am getting a NullReferenceExecption when I try to execute the javascript. Any help would be appreciated!

  7. Manuel

    Hi Adam Thanks for your code
    I was unable to get Javascript to run on iOS. Everytime it just stands still and javascript doesn’t react on the iOS device. I’ve tried on iPhone 5 and 6. Any help would be appreciated
    BTW Android device works like a charm!

      1. Manuel

        Hi Adam,

        Thanks for your reply. I found issue in iOS. I added a few keys in the NSTransport to allow documents and it worked

        Manuel

  8. Dan

    Hi Adam,

    Could you please explain your example a little further? I run the sample project and it opens up google in the webview. The Back and Refresh buttons work but the Eval JS does nothing (as far as I can tell).

    Could you please give a hint on how to use this?

    Thanks!

    1. Adam Pedley

      The EvalJS, just runs Javascript in the WebView. The sample I placed in the code, doesn’t actually do anything, it’s up to you to decide what you want.

      But just run any JS you want, in that function, and it will execute in the WebView.

  9. Neva

    Hi,Adam,your article has been a great help to me.But,version 4.2 of Android or version 4.3 of android, I got a error:
    Java.Lang.NoSuchMethodError: no method with name=’evaluateJavascript’ signature='(Ljava/lang/String;Landroid/webkit/ValueCallback;)V’ in class Landroid/webkit/WebView;
    in the code: Control?.EvaluateJavascript(js, new JavascriptCallback((r) => { response = r; reset.Set(); }));
    I tried to search the answer,and use :Control?.LoadUrl(string.Format(“javascript: {0}”, js)); the code replaced, there is no error ,but js don’t work; If you know the answer, tell me pls. Thank you very much.

    1. Adam Pedley

      Apologies for the delay in replying, your comment was caught in my spam filter.

      evaluateJavascript in Android was only added in API 19 (or Android 4.4+) You need to have a minimum target of this or higher to use it. Considering below API 19 now only accounts for less than 9% of the Android ecosystem, most people only support API 19+

Leave A Comment?