HybridWebview CanGoBack & GoBack using MVVM

David 356 Reputation points
2021-05-20T15:56:52.86+00:00

Hi

I have got a working HybridWebview used in my mainView and I have added a back button that I would like to bind to the hybridWebView History implementing cangoback and goback, so the button can be enable accordingly. I have done similar things with other buttons and using commands in the viewmodel, but the goback method is a tricky one for me. I would like to use it on Android and iOS and I am very stuck.

This is my typical HybridWebView based on Microsoft guidelines:

  public class HybridWebView : WebView
    {
        private Action<string, string> _action;

        public static readonly BindableProperty UriProperty = BindableProperty.Create(
           propertyName: "Uri",
           returnType: typeof(string),
           declaringType: typeof(HybridWebView),
           defaultValue: default(string));

        public string Uri
        {
            get { return (string)GetValue(UriProperty); }
            set { SetValue(UriProperty, value); }
        }

        public static readonly BindableProperty IsLoggedInProperty = BindableProperty.Create(
           propertyName: "IsLoggedIn",
           returnType: typeof(bool),
           declaringType: typeof(HybridWebView),
           defaultValue: default(bool));

        public bool IsLoggedIn
        {
            get { return (bool)GetValue(IsLoggedInProperty); }
            set { SetValue(IsLoggedInProperty, value); }
        }

        public void RegisterAction(Action<string, string> callback)
        {
            _action = callback;
        }

        public void Cleanup()
        {
            _action = null;
        }

        public void InvokeAction(string param1, string param2)
        {
            if (_action == null || (param1 == null && param2 == null))
            {
                return;
            }

            if (MainThread.IsMainThread)
                _action.Invoke(param1, param2);
            else
                MainThread.BeginInvokeOnMainThread(() => _action.Invoke(param1, param2));
        }
    }

Android Renderer:

    public class HybridWebViewRenderer : WebViewRenderer
    {
        private const string JavascriptFunction = "function invokeXamarinFormsAction(data){jsBridge.invokeAction(data);}";
        Context _context;

        public HybridWebViewRenderer(Context context) : base(context)
        {
            _context = context;
        }

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

            if (e.OldElement != null)
            {
                Control.RemoveJavascriptInterface("jsBridge");
                ((HybridWebView)Element).Cleanup();
            }
            if (e.NewElement != null)
            {
                Control.SetWebViewClient(new JavascriptWebViewClient(this, $"javascript: {JavascriptFunction}"));
                Control.AddJavascriptInterface(new JsBridge(this), "jsBridge");               

            }


        }



    }

Android Client:

    public class JavascriptWebViewClient : WebViewClient
    {
        readonly string _javascript;
        readonly HybridWebViewRenderer _renderer;

        public JavascriptWebViewClient(HybridWebViewRenderer hybridWebViewRenderer, string javascript)
        {
            _javascript = javascript;
            _renderer = hybridWebViewRenderer;
        }
        public override void OnReceivedSslError(Android.Webkit.WebView view, SslErrorHandler handler, Android.Net.Http.SslError error)
        {
            //base.OnReceivedSslError(view, handler, error);
            handler.Proceed();
        }
        public override void OnPageStarted(WebView view, string url, Android.Graphics.Bitmap favicon)
        {
            base.OnPageStarted(view, url, favicon);

        }
        public override void OnPageFinished(WebView view, string url)
        {
            base.OnPageFinished(view, url);
            view.EvaluateJavascript(_javascript, null);

            if (_renderer != null)
            {
                ((Controls.HybridWebView)_renderer.Element).IsLoggedIn = HasRotaOneCookie(url);              
            }
        }
.....

iOS Renderer:

  public class HybridWebViewRenderer : WkWebViewRenderer, IWKScriptMessageHandler
    {
        private const string JavaScriptFunction = "function invokeXamarinFormsAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";
        private WKUserContentController _userController;

        public HybridWebViewRenderer() : this(new WKWebViewConfiguration())
        {
        }

        public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
        {
            _userController = config.UserContentController;
            var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
            _userController.AddUserScript(script);
            _userController.AddScriptMessageHandler(this, "invokeAction");

        }

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

            if (e.OldElement != null)
            {
                _userController.RemoveAllUserScripts();
                _userController.RemoveScriptMessageHandler("invokeAction");
                HybridWebView hybridWebViewMain = e.OldElement as HybridWebView;
                hybridWebViewMain?.Cleanup();
            }

            if (e.NewElement != null)
            {
                //// No need this since we're loading dynamically generated HTML content
                //string filename = Path.Combine(NSBundle.MainBundle.BundlePath, $"Content/{((HybridWebView)Element).Uri}");
                //LoadRequest(new NSUrlRequest(new NSUrl(filename, false)));

                this.NavigationDelegate = new NavigationDelegate(this);                
            }

        }

        public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
        {
            var dataBody = message.Body.ToString();
            if (dataBody.Contains("|"))
            {
                var paramArray = dataBody.Split("|");
                var param1 = paramArray[0];
                var param2 = paramArray[1];
                ((HybridWebView)Element).InvokeAction(param1, param2);
            }
            else
            {
                ((HybridWebView)Element).InvokeAction(dataBody, null);
            }
        }

    }

    public class NavigationDelegate : WKNavigationDelegate
    {

        HybridWebViewRenderer _renderer;

        public NavigationDelegate(HybridWebViewRenderer renderer)
        {
            this._renderer = renderer;
        }

        public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
        {
            base.DidFinishNavigation(webView, navigation);

            HybridWebView webview = _renderer.Element as HybridWebView;
            webview.IsLoggedIn = HasRotaOneCookie();


        }
.....

iOS Delegate:

    [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {
        //
        // This method is invoked when the application has loaded and is ready to run. In this 
        // method you should instantiate the window, load the UI into it and then make the window
        // visible.
        //
        // You have 17 seconds to return from this method, or iOS will terminate your application.
        //
        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();
            LoadApplication(new App());

            return base.FinishedLaunching(app, options);
        }
    }

Then in the shared code, I have got the typical mainViewModel, how can I bind the back button from my view to do webVIew.Goback and being enabled if webView.CangoBack is true?

Thanks.

Developer technologies | .NET | Xamarin
{count} vote

Accepted answer
  1. JessieZhang-MSFT 7,716 Reputation points Microsoft External Staff
    2021-05-25T09:58:22.057+00:00

    Hello,

    Welcome to our Microsoft Q&A platform!

    You can use a custom Xamarin Forms WebView with custom CanGoBack/Forward properties. You can find the it here: https://github.com/nirbil/XF.CanGoWebView

    The main code is:

    CustomWebView control with new bindable properties:

    public class CustomWebView : WebView  
    {  
        public CustomWebView() {}  
        public static BindableProperty CustomCanGoForwardProperty =  
            BindableProperty.Create(  
                nameof(CustomCanGoForward),  
                typeof(bool),  
                typeof(CustomWebView),  
                false,  
                BindingMode.OneWayToSource);  
      
        public static BindableProperty CustomCanGoBackProperty =  
            BindableProperty.Create(  
                nameof(CustomCanGoBack),  
                typeof(bool),  
                typeof(CustomWebView),  
                defaultValue: false,  
                BindingMode.OneWayToSource);  
      
        public bool CustomCanGoForward  
        {  
            get => (bool)GetValue(CustomCanGoForwardProperty);  
            set => SetValue(CustomCanGoForwardProperty, value);  
        }  
      
        public bool CustomCanGoBack  
        {  
            get => (bool)GetValue(CustomCanGoBackProperty);  
            set => SetValue(CustomCanGoBackProperty, value);  
        }  
    }  
    

    Custom WebViewRenderer on Android:

    public class CustomWebViewRenderer : WebViewRenderer  
    {  
        protected override WebViewClient GetWebViewClient()  
        {  
            CustomWebViewClient webViewClient = new CustomWebViewClient(this);  
            webViewClient.AddressChanged += AddressChanged;  
            return webViewClient;  
        }  
      
        private void AddressChanged(string url)  
        {  
            if (Element is CustomWebView customWebView && Control != null)  
            {  
                customWebView.CustomCanGoBack = Control.CanGoBack();  
                customWebView.CustomCanGoForward = Control.CanGoForward();  
            }  
        }  
    }  
    

    class CustomWebViewClient.cs

    public class CustomWebViewClient: FormsWebViewClient  
    {  
        public delegate void AddressChangedEventHandler(string url);  
        public event AddressChangedEventHandler AddressChanged;  
        public CustomWebViewClient(WebViewRenderer renderer) : base(renderer) {}  
        public override void DoUpdateVisitedHistory(WebView view, string url, bool isReload)  
        {  
            base.DoUpdateVisitedHistory(view, url, isReload);  
            AddressChanged?.Invoke(view.Url);  
        }  
    }  
    

    Refer:https://stackoverflow.com/questions/59198171/xamarin-forms-webview-cangoforward-property-is-inaccurate-on-sites-that-utilize/59396144#59396144

    Best Regards,

    Jessie Zhang

    ---
    If the response is helpful, please click "Accept Answer" and upvote it.

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    2 people found this answer helpful.

1 additional answer

Sort by: Most helpful
  1. David 356 Reputation points
    2021-05-21T10:06:18.52+00:00

    Hi @JessieZhang-MSFT

    Thanks for your reply.
    I did not post HasRotaOneCookie method and other code because I thought it was superfluous for this question and it can be ignored

    I have got a regular button and a hybrid webview control on a stack layout. I would like to use said button as a back button to navigate through the previous webview history like a browser would do on any device, and be disabled when _webView.CanGoBack is false.

    In my mainViewModel I tried this code:

       public MainViewModel(HybridWebView webView)  
            {  
                _webView = webView;  
      
                _webView.RegisterAction(ExecuteActionFromJavascript);            
                _webView.Source = ConstantsHelper.BASE_URL;  
              
      
                  
                RefreshCommand = new AsyncCommand(() => RefreshExecute());  
                GoToPrevPageCommand = new AsyncCommand(() => GoToPrevPageExecute());   
            }  
      
            private async Task GoToPrevPageExecute()  
            {  
                if (_webView.CanGoBack)  
                {  
                    _webView.GoBack();  
                }  
            }  
    

    the snippet code of MainView.xaml:

        <StackLayout Spacing="0">        
            <RefreshView IsRefreshing="{Binding IsRefreshing}" Command="{Binding RefreshCommand}" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" RefreshColor="#F9A240">              
                <controls:HybridWebView  
                    x:Name="webViewElement"  
                    IsLoggedIn="{Binding IsLoggedIn, Mode=OneWayToSource}"  
                    HorizontalOptions="FillAndExpand"  
                    VerticalOptions="FillAndExpand" />  
            </RefreshView>  
      
      
                <Button Grid.Column="0" FontFamily="FA-S" Text="{StaticResource IconBack}" TextTransform="None"  TextColor="White" FontSize="Large" Command="{Binding GoToPrevPageCommand}"/>  
    </StackLayout>        
    

    The issue:
    I would like to make the back button work for android and ios devices and also use canexecute command so the button is disabled when it cannot go back.

    _webView.CanGoBack is always false.

    Thanks

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.