WebView 和错误管理,怎么了?

WebView and error management, what's wrong?

我正在开发 Xamarin.Forms 应用程序,我需要在其中集成 WebView 以通过外部 管理预订 URL.

所以在我看来基本上我已经这样做了:

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

我想解决用户在打开此页面时可能遇到的一些错误:无法访问互联网、超时、服务器不可用……

为此,我使用 EventToCommandBehavior 访问 ViewModel 中的事件 NavigatingNavigating

所以我的 XAML 看起来像这样:

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

ViewModel 是这样的:

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;
}

如果我使用右 URL,在 iOS 和 Android 上一切正常。

但是,如果我使用“错误的”URL(例如缺少字符),这仅适用于 Android:这种情况WebNavigationResult.FailureWebViewNavigatedAsync()中被捕获,但我在iOS.

中没有进入WebViewNavigatedAsync()

=>这正常吗?

我实现了一个“刷新”按钮来管理“无法访问互联网”错误。这个按钮可以通过 ToolBarItem 访问,在 ViewModel 中是这样的:

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

但在这些情况下,我在激活飞行模式后也有两种不同的行为:

=> 这正常吗?有没有合适的方法来管理这个?

is this normal?

根据我的测试。是的,我得到了相同的结果,如果我们输入错误 url,webview 始终是 white-empty 在 iOS 中的视图。所以 NavigatedCommand 无法执行。

如果我们使用正确的 url,webview 可以执行 NavigatedCommand,并且 运行 结果如下图所示。

Is there a proper way to manager this?

我们可以在 iOS 中为 webview 使用自定义渲染器来处理这种情况。

[assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWebViewRenderer))]
namespace MyCarsourlView.iOS
{
    [Obsolete]
    class CustomWebViewRenderer : WebViewRenderer
    {

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

            if (e.OldElement == null) { Delegate = new CustomWebViewDelegate(); }
        }
    }
}

internal class CustomWebViewDelegate : UIWebViewDelegate
{

    #region Event Handlers

    public override bool ShouldStartLoad(UIWebView webView, NSUrlRequest request, UIWebViewNavigationType navigationType)
    {

        //Could add stuff here to redirect the user before the page loads, if you wanted to redirect the user you would do the redirection and then return false

        return true;
    }

    public override void LoadFailed(UIWebView webView, NSError error)
    {
        Console.WriteLine("\nIn AppName.iOS.CustomWebViewRenderer - Error: {0}\n", error.ToString());   //TODO: Do something more useful here
        //Here, you can test to see what kind of error you are getting and act accordingly, either by showing an alert or rendering your own custom error page using basic HTML
    }

    public override void LoadingFinished(UIWebView webView)
    { //You could do stuff here such as collection cookies form the WebView }

        #endregion
    }
}

如果我输入错误url,LoadFailed会被执行

我发现了另一种似乎有效的方法,基于以下链接: https://forums.xamarin.com/discussion/99790/xamarin-forms-custom-webviews-navigating-and-navigated-event-doesnt-raise-on-ios

https://gist.github.com/mrdaneeyul/9cedb3d972744de4f8752cb09024da42

它可能并不完美,尤其是当我需要从 ViewModel 访问所需的事件时。

所以我创建了一个继承自 WebViewCustomWebView 控件:

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());
    }
}

然后我有自定义 CustomWebView:

行为的 CustomWkWebViewRenderer
[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)));
        }
    }
}

我还有 CustomWkWebViewNavigationDelegate 实现了这个渲染器的 WKNavigationDelegate

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

有一个 CustomWebViewErrorKind 枚举可以让我在 ViewModel 中实现一个常见的错误管理:

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

为了从 ViewModel 访问事件,我使用 EventToCommandBehavior 描述 there

所以,我已经公开了视图中的所有命令,如下所示:

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

最后,在我的 ViewModel 中,我为 Android 部分执行此操作:

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;
}

我为 iOS 部分这样做:

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;
}

像这样我能够管理错误,从 ViewModel 重试和刷新,即使它可能不是更好的解决方案...