Forum Xamarin.Forms

Announcement:

The Xamarin Forums have officially moved to the new Microsoft Q&A experience. Microsoft Q&A is the home for technical questions and answers at across all products at Microsoft now including Xamarin!

To create new threads and ask questions head over to Microsoft Q&A for .NET and get involved today.

WebView and error management, what's wrong?

PacodosoPacodoso FRUniversity ✭✭✭
edited October 2020 in Xamarin.Forms

I'm working on a Xamarin.Forms app where I need to integrate a WebView to manage booking through an external URL.

So basically I've did this in my view:

<WebView x:Name="webView" Source="{Binding BookingUrl}"
         WidthRequest="1000" HeightRequest="1000">

I would like to manage some errors that the users could encounter while opening this page: no internet access, timeout, unavailable server,...

For this I've used EventToCommandBehaviorto acess to the events Navigating and Navigating in the ViewModel.

So my XAML looks like this:

<WebView x:Name="webView" Source="{Binding AvilaUrlBooking}"
            WidthRequest="1000" HeightRequest="1000">
    <WebView.Behaviors>
        <behaviors:EventToCommandBehavior
            EventName="Navigating"
            Command="{Binding NavigatingCommand}" />
        <behaviors:EventToCommandBehavior
            EventName="Navigated"
            Command="{Binding NavigatedCommand}" />
    </WebView.Behaviors>
</WebView>

And the ViewModel is like this:

public ICommand NavigatingCommand
{
    get
    {
        return new Xamarin.Forms.Command<WebNavigatingEventArgs>(async (x) =>
        {
            if (x != null)
            {
                await WebViewNavigatingAsync(x);
            }
        });
    }
}

private Task WebViewNavigatingAsync(WebNavigatingEventArgs eventArgs)
{
    if (!IsConnected)
        ServiceErrorKind = ServiceErrorKind.NoInternetAccess;

    IsBusy = true;
    return Task.CompletedTask;
}

public ICommand NavigatedCommand
{
    get
    {
        return new Xamarin.Forms.Command<WebNavigatedEventArgs>(async (x) =>
        {
            if (x != null)
            {
                await WebViewNavigatedAsync(x);
            }
        });
    }
}

private Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
{
    IsBusy = false;
    IsFirstDisplay = false;
    switch (eventArgs.Result)
    {
        case WebNavigationResult.Cancel:
            // TODO - do stuff here
            break;
        case WebNavigationResult.Failure:
            // TODO - do stuff here
            break;
        case WebNavigationResult.Success:
            // TODO - do stuff here
            break;
        case WebNavigationResult.Timeout:
            // TODO - do stuff here
            break;
        default:
            // TODO - do stuff here
            break;
    }
    return Task.CompletedTask;
}

bool isFirstDisplay;
public bool IsFirstDisplay
{
    get { return isFirstDisplay; }
    set { SetProperty(ref isFirstDisplay, value); }
}

public BookingViewModel()
{
    _eventTracker = new AppCenterEventTracker();
    IsFirstDisplay = true;
    Title = "Booking";

    IsConnected = Connectivity.NetworkAccess == NetworkAccess.Internet;
    Connectivity.ConnectivityChanged += OnConnectivityChanged;
}

If I use the right URL, all works fine on iOS and Android.

However, if I use a "wrong" URL (with missing char for example), this is only working on Android: the case WebNavigationResult.Failure is catched in WebViewNavigatedAsync(), but I don't enter in WebViewNavigatedAsync() on iOS.

=> is this normal?

I've implemented a "Refresh" button to manage the "No Internet access" error. This button is accessible through a ToolBarItem, it's like this in the ViewModel:

public void Refresh(object sender)
{
    try
    {
        var view = sender as Xamarin.Forms.WebView;
        view.Reload();
    }
    catch (Exception ex)
    {
    }
}

But in these case too, I have 2 different behaviours after having activated the Airplane mode:

  • on iOS, when there is no internet access: I don't enter in WebViewNavigatedAsync(), even if the internet access is available again and I click on the "Refresh" button, I only pass by the WebViewNavigatingAsync()
  • on Android, when there is no internet access: I well enter in WebViewNavigatedAsync(), and when the internet access is available again and I click on the "Refresh" button, I pass both by the WebViewNavigatingAsync() and WebViewNavigatedAsync()

=> is this normal? Is there a proper way to manager this?

Best Answer

  • PacodosoPacodoso FRUniversity ✭✭✭
    Accepted Answer

    I've found another approach that seems to work, based on the following links:
    https://forums.xamarin.com/discussion/99790/xamarin-forms-custom-webviews-navigating-and-navigated-event-doesnt-raise-on-ios![](https://us.v-cdn.net/5019960/uploads/editor/bf/doxtxuevf9lp.png "")

    https://gist.github.com/mrdaneeyul/9cedb3d972744de4f8752cb09024da42![](https://us.v-cdn.net/5019960/uploads/editor/vy/4y4csv0e3fe4.png "")

    It's probably not perfect, especially as I need access to the required Events from the ViewModel.

    So I've created a CustomWebView control that inherits from WebView:

    public class CustomWebView : WebView
    {
        public static readonly BindableProperty UriProperty = BindableProperty.Create(
            propertyName: "Uri",
            returnType: typeof(string),
            declaringType: typeof(CustomWebView),
            defaultValue: default(string));
    
        public string Uri
        {
            get { return (string)GetValue(UriProperty); }
            set { SetValue(UriProperty, value); }
        }
    
        public CustomWebViewErrorKind ErrorKind { get; set; }
    
        public event EventHandler LoadingStart;
        public event EventHandler LoadingFinished;
        public event EventHandler LoadingFailed;
    
        /// <summary>
        /// The event handler for refreshing the page
        /// </summary>
        public EventHandler OnRefresh { get; set; }
    
        public void InvokeCompleted()
        {
            if (this.LoadingFinished != null)
            {
                ErrorKind = WebViewErrorKind.None;
                this.LoadingFinished.Invoke(this, null);
            }
        }
    
        public void InvokeStarted()
        {
            if (this.LoadingStart != null)
            {
                ErrorKind = WebViewErrorKind.None;
                this.LoadingStart.Invoke(this, null);
            }
        }
    
        public void InvokeFailed(CustomWebViewErrorKind errorKind)
        {
            if (this.LoadingFailed != null)
            {
                ErrorKind = errorKind;
                this.LoadingFailed.Invoke(this, null);
            }
        }
    
        /// <summary>
        /// Refreshes the current page
        /// </summary>
        public void Refresh()
        {
            OnRefresh?.Invoke(this, new EventArgs());
        }
    }
    

    Then I've the CustomWkWebViewRenderer that customizes the behavior of the CustomWebView:

    [assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWkWebViewRenderer))]
    namespace MyProject.iOS.Renderers
    {
        public class CustomWkWebViewRenderer : ViewRenderer<CustomWebView, WKWebView>
        {
    
            public CustomWkWebViewRenderer()
            {
                Debug.WriteLine($"CustomWkWebViewRenderer - Ctor");
            }
    
            WKWebView webView;
    
            protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
            {
                base.OnElementChanged(e);
                Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged()");
    
                if (Control == null)
                {
                    Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged() - Control == null");
                    webView = new WKWebView(Frame, new WKWebViewConfiguration()
                    {
                        MediaPlaybackRequiresUserAction = false
                    });
                    webView.NavigationDelegate = new DisplayWebViewDelegate(Element);
                    SetNativeControl(webView);
    
                    Element.OnRefresh += (sender, ea) => Refresh(sender);
                }
                if (e.NewElement != null)
                {
                    Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged() - e.NewElement != null");
                    Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
                    webView.NavigationDelegate = new DisplayWebViewDelegate(Element);
                    SetNativeControl(webView);
                }
            }
    
            private void Refresh(object sender)
            {
                Debug.WriteLine($"CustomWkWebViewRenderer - Refresh()");
                Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
            }
        }
    }
    

    I also have the CustomWkWebViewNavigationDelegate that implements the WKNavigationDelegate for this renderer:

        public class CustomWkWebViewNavigationDelegate : WKNavigationDelegate
        {
            private CustomWebView element;
    
            public CustomWkWebViewNavigationDelegate(CustomWebView element)
            {
                Debug.WriteLine($"CustomWkWebViewNavigationDelegate - Ctor");
                this.element = element;
            }
    
            public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
            {
                Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidFinishNavigation");
                element.InvokeCompleted();
                //base.DidFinishNavigation(webView, navigation);
            }
    
            public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation)
            {
                Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidStartProvisionalNavigation");
                element.InvokeStarted();
                //base.DidStartProvisionalNavigation(webView, navigation);
            }
    
            [Export("webView:didFailProvisionalNavigation:withError:")]
            public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error)
            {
                Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidFailProvisionalNavigation - error : {error}");
                var errorKind = CustomWebViewErrorKind.None;
                switch (error.Code)
                {
                    case -1009: // no internet access
                        {
                            errorKind = CustomWebViewErrorKind.NoInternetAccess;
                            break;
                        }
                    case -1001: // timeout
                        {
                            errorKind = CustomWebViewErrorKind.Timeout;
                            break;
                        }
                    case -1003: // server cannot be found
                    case -1100: // url not found on server
                    default:
                        {
                            errorKind = CustomWebViewErrorKind.Failure;
                            break;
                        }
                }
                element.InvokeFailed(errorKind);
                //base.DidFailProvisionalNavigation(webView, navigation, error);
            }
        }
    

    There is a CustomWebViewErrorKind enum that will allow me to implement a common error management in the ViewModel:

    public enum CustomWebViewErrorKind
    {
        None = 0,
        NoInternetAccess = 1,
        Failure = 2,
        Timeout = 3,
        Cancel = 8,
        Other = 9
    }
    

    To access to the Events from the ViewModel, I use a EventToCommandBehavior like described there

    So, I've exposed all the Commands from the View like this:

    <controls:CustomWebView x:Name="webView"
                            Source="{Binding MyUrlBooking}"
                            Uri="{Binding MyUrlBooking}"
                            WidthRequest="1000" HeightRequest="1000">
        <WebView.Behaviors>
            <behaviors:EventToCommandBehavior
                EventName="Navigating"
                Command="{Binding NavigatingCommand}" />
            <behaviors:EventToCommandBehavior
                EventName="Navigated"
                Command="{Binding NavigatedCommand}" />
            <behaviors:EventToCommandBehavior
                EventName="LoadingStart"
                Command="{Binding LoadingStartCommand}" />
            <behaviors:EventToCommandBehavior
                EventName="LoadingFinished"
                Command="{Binding LoadingFinishedCommand}" />
            <behaviors:EventToCommandBehavior
                EventName="LoadingFailed"
                Command="{Binding LoadingFailedCommand}"
                CommandParameter="{x:Reference webView}"
                />
        </WebView.Behaviors>
    </controls:CustomWebView>
    

    And finally, in my ViewModel I do this for the Android part:

    public ICommand NavigatingCommand
    {
        get
        {
            return new Xamarin.Forms.Command<WebNavigatingEventArgs>(async (x) =>
            {
                if (x != null)
                {
                    await WebViewNavigatingAsync(x);
                }
            });
        }
    }
    
    private Task WebViewNavigatingAsync(WebNavigatingEventArgs eventArgs)
    {
        Debug.WriteLine($"BookingViewModel - WebViewNavigatingAsync()");
    
        IsBusy = true;
        return Task.CompletedTask;
    }
    
    public ICommand NavigatedCommand
    {
        get
        {
            return new Xamarin.Forms.Command<WebNavigatedEventArgs>(async (x) =>
            {
                if (x != null)
                {
                    await WebViewNavigatedAsync(x);
                }
            });
        }
    }
    
    private Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
    {
        Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync()");
    
        IsBusy = false;
        switch (eventArgs.Result)
        {
            case WebNavigationResult.Cancel:
                Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Cancel");
                ErrorKind = CustomWebViewErrorKind.Cancel;
                break;
            case WebNavigationResult.Failure:
            default:
                Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure");
                IsConnected = Connectivity.NetworkAccess == NetworkAccess.Internet;
                if (IsConnected)
                {
                    Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure : Failure");
                    ErrorKind = CustomWebViewErrorKind.Failure;
                }
                else
                if (IsConnected)
                {
                    Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure : NoInternetAccess");
                    ErrorKind = CustomWebViewErrorKind.NoInternetAccess;
                }
                break;
            case WebNavigationResult.Success:
                Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Success");
                ErrorKind = CustomWebViewErrorKind.None;
                IsFirstDisplay = false;
                break;
            case WebNavigationResult.Timeout:
                Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Timeout");
                ErrorKind = CustomWebViewErrorKind.Timeout;
                break;
        }
        return Task.CompletedTask;
    }
    
    

    And I do this for the iOS part:

    public ICommand LoadingStartCommand
    {
        get
        {
            return new Xamarin.Forms.Command(async () =>
            {
                await WebViewLoadingStartAsync();
            });
        }
    }
    
    private Task WebViewLoadingStartAsync()
    {
        Debug.WriteLine($"BookingViewModel - WebViewLoadingStartAsync()");
        IsBusy = true;
        return Task.CompletedTask;
    }
    
    public ICommand LoadingFinishedCommand
    {
        get
        {
            return new Xamarin.Forms.Command(async () =>
            {
                await WebViewLoadingFinishedAsync();
            });
        }
    }
    
    private Task WebViewLoadingFinishedAsync()
    {
        Debug.WriteLine($"BookingViewModel - WebViewLoadingFinishedAsync()");
        IsBusy = false;
        return Task.CompletedTask;
    }
    
    public ICommand LoadingFailedCommand
    {
        get
        {
            return new Xamarin.Forms.Command<object>(async (object sender) =>
            {
                if (sender != null)
                {
                    await WebViewLoadingFailedAsync(sender);
                }
            });
        }
    }
    
    private Task WebViewLoadingFailedAsync(object sender)
    {
        Debug.WriteLine($"BookingViewModel - WebViewLoadingFailedAsync()");
        var view = sender as CustomWebView;
        var error = view.ErrorKind;
        Debug.WriteLine($"BookingViewModel - WebViewLoadingFailedAsync() - error : {error}");
        IsBusy = false;
        return Task.CompletedTask;
    }
    

    Like this I'm able to manage errors, retry and refresh from the ViewModel, even if it's probably not the better solution...

Answers

  • ColeXColeX Member, Xamarin Team Xamurai
    edited November 2020

    I did a test and managed to reproduce the issue .

    The webview gets totally white screen when requesting a invalid url , please consider raising the issue on github : https://github.com/xamarin/Xamarin.Forms/issues.

    Workaround : handle the navigation error in custom renderer ,the method LoadFailed would triggers in this scenario.

    [assembly: ExportRenderer(typeof(Xamarin.Forms.WebView), typeof(MyRenderer))]
    namespace FormsApp.iOS
    {
        class MyRenderer : WkWebViewRenderer
        {
    
            protected override void OnElementChanged(VisualElementChangedEventArgs e)
            {
                base.OnElementChanged(e);
                if(e.NewElement != null)
                {
                    NavigationDelegate = new My();
                }
            }
        }
    
        public class My : WKNavigationDelegate
        {
            public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error)
            {
                //failed
            }
        }
    }
    
  • PacodosoPacodoso FRUniversity ✭✭✭
    edited November 2020

    Thanks @ColeX

    Your solution is working, but it's not exactly what I need.

    It's not possible to use a common iOS/Android code in the ViewModel, to identify the errors (wrong URL, timeout, no internet, ...) and to manage them (full screen error view with a "Retry" button, pop in)...

    Isn't there a way to fire the WebView.Nagitated event as soon as an error has been catched inLoadFailed()?

  • ColeXColeX Member, Xamarin Team Xamurai

    You could use error.code to specify the cause in didFailProvisionalNavigation method .

    public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error)
            {
                if (error.Code == -1001) { // TIMED OUT:
    
                    // CODE to handle TIMEOUT
    
                }
                else if (error.Code == -1003 ){ // SERVER CANNOT BE FOUND
    
                    // CODE to handle SERVER not found
    
                }
                else if( error.Code == -1100 ){ // URL NOT FOUND ON SERVER
    
                    // CODE to handle URL not found
    
                }
            }
    

    Refer to

    https://stackoverflow.com/a/33543173/8187800

  • PacodosoPacodoso FRUniversity ✭✭✭

    @ColeX Thank you.
    I see how it works, but how to share the error with the ViewModel? The MessagingCenter seems to be the only option?

  • ColeXColeX Member, Xamarin Team Xamurai

    @Pacodoso said:
    @ColeX Thank you.
    I see how it works, but how to share the error with the ViewModel? The MessagingCenter seems to be the only option?

    I'm afraid so ..

  • PacodosoPacodoso FRUniversity ✭✭✭
    Accepted Answer

    I've found another approach that seems to work, based on the following links:
    https://forums.xamarin.com/discussion/99790/xamarin-forms-custom-webviews-navigating-and-navigated-event-doesnt-raise-on-ios![](https://us.v-cdn.net/5019960/uploads/editor/bf/doxtxuevf9lp.png "")

    https://gist.github.com/mrdaneeyul/9cedb3d972744de4f8752cb09024da42![](https://us.v-cdn.net/5019960/uploads/editor/vy/4y4csv0e3fe4.png "")

    It's probably not perfect, especially as I need access to the required Events from the ViewModel.

    So I've created a CustomWebView control that inherits from WebView:

    public class CustomWebView : WebView
    {
        public static readonly BindableProperty UriProperty = BindableProperty.Create(
            propertyName: "Uri",
            returnType: typeof(string),
            declaringType: typeof(CustomWebView),
            defaultValue: default(string));
    
        public string Uri
        {
            get { return (string)GetValue(UriProperty); }
            set { SetValue(UriProperty, value); }
        }
    
        public CustomWebViewErrorKind ErrorKind { get; set; }
    
        public event EventHandler LoadingStart;
        public event EventHandler LoadingFinished;
        public event EventHandler LoadingFailed;
    
        /// <summary>
        /// The event handler for refreshing the page
        /// </summary>
        public EventHandler OnRefresh { get; set; }
    
        public void InvokeCompleted()
        {
            if (this.LoadingFinished != null)
            {
                ErrorKind = WebViewErrorKind.None;
                this.LoadingFinished.Invoke(this, null);
            }
        }
    
        public void InvokeStarted()
        {
            if (this.LoadingStart != null)
            {
                ErrorKind = WebViewErrorKind.None;
                this.LoadingStart.Invoke(this, null);
            }
        }
    
        public void InvokeFailed(CustomWebViewErrorKind errorKind)
        {
            if (this.LoadingFailed != null)
            {
                ErrorKind = errorKind;
                this.LoadingFailed.Invoke(this, null);
            }
        }
    
        /// <summary>
        /// Refreshes the current page
        /// </summary>
        public void Refresh()
        {
            OnRefresh?.Invoke(this, new EventArgs());
        }
    }
    

    Then I've the CustomWkWebViewRenderer that customizes the behavior of the CustomWebView:

    [assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWkWebViewRenderer))]
    namespace MyProject.iOS.Renderers
    {
        public class CustomWkWebViewRenderer : ViewRenderer<CustomWebView, WKWebView>
        {
    
            public CustomWkWebViewRenderer()
            {
                Debug.WriteLine($"CustomWkWebViewRenderer - Ctor");
            }
    
            WKWebView webView;
    
            protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
            {
                base.OnElementChanged(e);
                Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged()");
    
                if (Control == null)
                {
                    Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged() - Control == null");
                    webView = new WKWebView(Frame, new WKWebViewConfiguration()
                    {
                        MediaPlaybackRequiresUserAction = false
                    });
                    webView.NavigationDelegate = new DisplayWebViewDelegate(Element);
                    SetNativeControl(webView);
    
                    Element.OnRefresh += (sender, ea) => Refresh(sender);
                }
                if (e.NewElement != null)
                {
                    Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged() - e.NewElement != null");
                    Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
                    webView.NavigationDelegate = new DisplayWebViewDelegate(Element);
                    SetNativeControl(webView);
                }
            }
    
            private void Refresh(object sender)
            {
                Debug.WriteLine($"CustomWkWebViewRenderer - Refresh()");
                Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
            }
        }
    }
    

    I also have the CustomWkWebViewNavigationDelegate that implements the WKNavigationDelegate for this renderer:

        public class CustomWkWebViewNavigationDelegate : WKNavigationDelegate
        {
            private CustomWebView element;
    
            public CustomWkWebViewNavigationDelegate(CustomWebView element)
            {
                Debug.WriteLine($"CustomWkWebViewNavigationDelegate - Ctor");
                this.element = element;
            }
    
            public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
            {
                Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidFinishNavigation");
                element.InvokeCompleted();
                //base.DidFinishNavigation(webView, navigation);
            }
    
            public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation)
            {
                Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidStartProvisionalNavigation");
                element.InvokeStarted();
                //base.DidStartProvisionalNavigation(webView, navigation);
            }
    
            [Export("webView:didFailProvisionalNavigation:withError:")]
            public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error)
            {
                Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidFailProvisionalNavigation - error : {error}");
                var errorKind = CustomWebViewErrorKind.None;
                switch (error.Code)
                {
                    case -1009: // no internet access
                        {
                            errorKind = CustomWebViewErrorKind.NoInternetAccess;
                            break;
                        }
                    case -1001: // timeout
                        {
                            errorKind = CustomWebViewErrorKind.Timeout;
                            break;
                        }
                    case -1003: // server cannot be found
                    case -1100: // url not found on server
                    default:
                        {
                            errorKind = CustomWebViewErrorKind.Failure;
                            break;
                        }
                }
                element.InvokeFailed(errorKind);
                //base.DidFailProvisionalNavigation(webView, navigation, error);
            }
        }
    

    There is a CustomWebViewErrorKind enum that will allow me to implement a common error management in the ViewModel:

    public enum CustomWebViewErrorKind
    {
        None = 0,
        NoInternetAccess = 1,
        Failure = 2,
        Timeout = 3,
        Cancel = 8,
        Other = 9
    }
    

    To access to the Events from the ViewModel, I use a EventToCommandBehavior like described there

    So, I've exposed all the Commands from the View like this:

    <controls:CustomWebView x:Name="webView"
                            Source="{Binding MyUrlBooking}"
                            Uri="{Binding MyUrlBooking}"
                            WidthRequest="1000" HeightRequest="1000">
        <WebView.Behaviors>
            <behaviors:EventToCommandBehavior
                EventName="Navigating"
                Command="{Binding NavigatingCommand}" />
            <behaviors:EventToCommandBehavior
                EventName="Navigated"
                Command="{Binding NavigatedCommand}" />
            <behaviors:EventToCommandBehavior
                EventName="LoadingStart"
                Command="{Binding LoadingStartCommand}" />
            <behaviors:EventToCommandBehavior
                EventName="LoadingFinished"
                Command="{Binding LoadingFinishedCommand}" />
            <behaviors:EventToCommandBehavior
                EventName="LoadingFailed"
                Command="{Binding LoadingFailedCommand}"
                CommandParameter="{x:Reference webView}"
                />
        </WebView.Behaviors>
    </controls:CustomWebView>
    

    And finally, in my ViewModel I do this for the Android part:

    public ICommand NavigatingCommand
    {
        get
        {
            return new Xamarin.Forms.Command<WebNavigatingEventArgs>(async (x) =>
            {
                if (x != null)
                {
                    await WebViewNavigatingAsync(x);
                }
            });
        }
    }
    
    private Task WebViewNavigatingAsync(WebNavigatingEventArgs eventArgs)
    {
        Debug.WriteLine($"BookingViewModel - WebViewNavigatingAsync()");
    
        IsBusy = true;
        return Task.CompletedTask;
    }
    
    public ICommand NavigatedCommand
    {
        get
        {
            return new Xamarin.Forms.Command<WebNavigatedEventArgs>(async (x) =>
            {
                if (x != null)
                {
                    await WebViewNavigatedAsync(x);
                }
            });
        }
    }
    
    private Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
    {
        Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync()");
    
        IsBusy = false;
        switch (eventArgs.Result)
        {
            case WebNavigationResult.Cancel:
                Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Cancel");
                ErrorKind = CustomWebViewErrorKind.Cancel;
                break;
            case WebNavigationResult.Failure:
            default:
                Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure");
                IsConnected = Connectivity.NetworkAccess == NetworkAccess.Internet;
                if (IsConnected)
                {
                    Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure : Failure");
                    ErrorKind = CustomWebViewErrorKind.Failure;
                }
                else
                if (IsConnected)
                {
                    Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure : NoInternetAccess");
                    ErrorKind = CustomWebViewErrorKind.NoInternetAccess;
                }
                break;
            case WebNavigationResult.Success:
                Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Success");
                ErrorKind = CustomWebViewErrorKind.None;
                IsFirstDisplay = false;
                break;
            case WebNavigationResult.Timeout:
                Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Timeout");
                ErrorKind = CustomWebViewErrorKind.Timeout;
                break;
        }
        return Task.CompletedTask;
    }
    
    

    And I do this for the iOS part:

    public ICommand LoadingStartCommand
    {
        get
        {
            return new Xamarin.Forms.Command(async () =>
            {
                await WebViewLoadingStartAsync();
            });
        }
    }
    
    private Task WebViewLoadingStartAsync()
    {
        Debug.WriteLine($"BookingViewModel - WebViewLoadingStartAsync()");
        IsBusy = true;
        return Task.CompletedTask;
    }
    
    public ICommand LoadingFinishedCommand
    {
        get
        {
            return new Xamarin.Forms.Command(async () =>
            {
                await WebViewLoadingFinishedAsync();
            });
        }
    }
    
    private Task WebViewLoadingFinishedAsync()
    {
        Debug.WriteLine($"BookingViewModel - WebViewLoadingFinishedAsync()");
        IsBusy = false;
        return Task.CompletedTask;
    }
    
    public ICommand LoadingFailedCommand
    {
        get
        {
            return new Xamarin.Forms.Command<object>(async (object sender) =>
            {
                if (sender != null)
                {
                    await WebViewLoadingFailedAsync(sender);
                }
            });
        }
    }
    
    private Task WebViewLoadingFailedAsync(object sender)
    {
        Debug.WriteLine($"BookingViewModel - WebViewLoadingFailedAsync()");
        var view = sender as CustomWebView;
        var error = view.ErrorKind;
        Debug.WriteLine($"BookingViewModel - WebViewLoadingFailedAsync() - error : {error}");
        IsBusy = false;
        return Task.CompletedTask;
    }
    

    Like this I'm able to manage errors, retry and refresh from the ViewModel, even if it's probably not the better solution...

Sign In or Register to comment.