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.

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) | Exrin | Xamarin Forms Developer | Melbourne, Australia | Open to sponsorship to Canada or US

Related Posts

5 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.

Leave A Comment?