Xamarin Forms WebView Bindable Actions

 

The WebView has a number of functions and properties that are only available by directly accessing the control.  To maintain a clean code behind and call functions from the ViewModel, we need to create a new custom control that provides new bindable properties.

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

Extended Control

First, lets create a new custom control that inherits from a WebView. Some of the most common usages are, Refresh, GoBack and a function that returns the result of CanGoBack.

public namespace Mobile.Control
{
    public class WebViewer : WebView
    {
        public static BindableProperty RefreshCommandProperty =
        BindableProperty.Create(nameof(RefreshCommand), typeof(Action), typeof(WebViewer), null, BindingMode.OneWayToSource);

        public Action RefreshCommand
        {
            get { return (Action)GetValue(RefreshCommandProperty); }
            set { SetValue(RefreshCommandProperty, value); }
        }

        public static BindableProperty GoBackCommandProperty =
        BindableProperty.Create(nameof(GoBackCommand), typeof(Action), typeof(WebViewer), null, BindingMode.OneWayToSource);

        public Action GoBackCommand
        {
            get { return (Action)GetValue(GoBackCommandProperty); }
            set { SetValue(GoBackCommandProperty, value); }
        }

        public static BindableProperty CanGoBackFunctionProperty =
        BindableProperty.Create(nameof(CanGoBackFunction), typeof(Func<bool>), typeof(WebViewer), null, BindingMode.OneWayToSource);

        public Func<bool> CanGoBackFunction
        {
            get { return (Func<bool>)GetValue(CanGoBackFunctionProperty); }
            set { SetValue(CanGoBackFunctionProperty, value); }
        }
    }
}

Android Renderer

Create this custom renderer in your Android project.

[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);

            if (Control != null && e.NewElement != null)
            {
                InitializeCommands((WebViewer)e.NewElement);
            }
        }

        private void InitializeCommands(WebViewer element)
        {
            element.RefreshCommand = () =>
            {
                Control?.Reload();
            };

            element.GoBackCommand = () =>
            {
                var ctrl = Control;
                if (ctrl == null)
                    return;

                if (ctrl.CanGoBack())
                    ctrl.GoBack();
            };

            element.CanGoBackFunction = () =>
            {
                var ctrl = Control;
                if (ctrl == null)
                    return false;

                return ctrl.CanGoBack();
            };
        }
    }    
}

iOS Renderer

Create this custom renderer in your iOS project.

[assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))]
namespace Mobile.iOS
{
    public class WebViewRender : WebViewRenderer
    {

        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (NativeView != null && e.NewElement != null)
                InitializeCommands((WebViewer)e.NewElement);

        }

        private void InitializeCommands(WebViewer element)
        {
            element.RefreshCommand = () =>
            {
                ((UIWebView)NativeView).Reload();
            };

            element.GoBackCommand = () =>
            {
                var control = ((UIWebView)NativeView);
                if (control.CanGoBack)
                {
                    element.IsBackNavigating = true;
                    control.GoBack();
                }
            };

            element.CanGoBackFunction = () =>
            {
                return ((UIWebView)NativeView).CanGoBack;
            };
        }
    }
}

UWP

Create this custom renderer in your UWP project.

[assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))]
namespace Mobile.UWP
{
    public class WebViewRender : WebViewRenderer
    {

        protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
        {
            base.OnElementChanged(e);

            if (Control != null && e.NewElement != null)
                InitializeCommands((WebViewer)e.NewElement);
        }
        private void InitializeCommands(WebViewer element)
        {
             element.RefreshCommand = () =>
             {
                 Control.Refresh();
             };

             element.GoBack = () =>
             {
                 if (Control.CanGoBack)
                     Control.GoBack();
             };

             element.CanGoBackFunction = () =>
             {
                 return Control.CanGoBack;
             };
        }
    }
}

Calling From ViewModel

First you need to create a number of properties in your ViewModel to bind to the new Commands.

private Action _refresh;
public Action Refresh
{
    get { return _refresh; }
    set { Set(value); }
}

private Action _goBack;
public Action GoBack
{
    get { return _goBack; }
    set { _goBack = value; }
}

private Func<bool> _canGoBack;
public Func<bool> CanGoBack
{
    get { return _canGoBack; }
    set { _canGoBack = value; }
}

In your view, add the following attribute, to ensure you can reference your new control.

xmlns:control="clr-namespace:Mobile.Control"

Now add in your control and bind each function to the property in your ViewModel. We only want a OneWayToSource binding here as the control manages the function and we don’t want out ViewModel to overwrite it.

<control:WebViewer Source="{Binding WebViewSource}"
                   CanGoBackFunction="{Binding CanGoBack, Mode=OneWayToSource}"
                   GoBackCommand="{Binding GoBackCommand, Mode=OneWayToSource}"
                   RefreshCommand="{Binding RefreshCommand, Mode=OneWayToSource}"
                   HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" />

To call any of these, you can now execute any Action or call any Function from the ViewModel.

// From anywhere in your ViewModel

GoBack();
var result = CanGoBack();
Refresh();
Microsoft MVP | Xamarin MVP | Xamarin Certified Developer |
Exrin MVVM Framework | Xamarin Forms Developer | Melbourne, Australia

Related Posts

3 Comments

  1. Mick George

    Hi Adam, I just stumbled on this posting today and I am wondering if i might be able to leverage something similar with my custom webview renderer. I have searched long and hard online for a solution but have come up short so far.

    My problem is automatically sizing a webview based upon the HTML source I assign it. I have a ListView DataTemplate that contains a ViewCell and within that a couple of labels and image and a webview. Each control is bound to a property on my ViewModel but the webview does not auto size correctly especially when paging the results. Would it be possible to add some sort of refresh or layout update in a content renderer?

    Thank you for your time.

    1. Adam Pedley

      Sizing the WebView based on its content is actually tricky business. What you will need to do is call Javascript on the webpage to determine its height. Get the value, then manually set the width and height of the control. AFAIK, there is no way for the control to autosize based on content.

      You will need to trigger the javascript, after the content has loaded. This might be harder, if the site loads with javascript, as the loaded event will then come back before the page has actually finished loading. If its simple html, you should only get the Loaded event once the page has been completely loaded.

      If you want to call Javascript on a WebView, I do have another article detailing that: https://xamarinhelp.com/xamarin-forms-webview-executing-javascript/

Leave A Comment?