question

David-7465 avatar image
1 Vote"
David-7465 asked David-7465 commented

HybridWebview CanGoBack & GoBack using MVVM

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.

dotnet-xamarin
· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Hi @David-7465 ,what's the code of HasRotaOneCookie in your code ? In addition, could you please post the whole code of your JSBridge ? I found it is different from the offical sample code in HybridWebView ?


0 Votes 0 ·
JessieZhang-2116 avatar image
2 Votes"
JessieZhang-2116 answered David-7465 commented

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.


· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Thank you very much for your help

0 Votes 0 ·
David-7465 avatar image
0 Votes"
David-7465 answered David-7465 edited

Hi @JessieZhang-2116

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

5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.