WebView和错误管理,怎么了?

问题描述

我正在使用 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)
    {
    }
}

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

  • iOS 上,当无法访问互联网时:即使再次可以访问互联网,我也不会输入WebViewNavigatedAsync(),然后单击“刷新“按钮,我只经过了WebViewNavigatingAsync()
  • Android 上,当无法访问互联网时:我很好地输入WebViewNavigatedAsync(),并且当再次可以访问互联网时,我点击“刷新”按钮,我同时通过了WebViewNavigatingAsync()WebViewNavigatedAsync()

=>这正常吗?有适当的方法来解决这个问题吗?

解决方法

这正常吗?

根据我的测试。是的,我得到了相同的结果,如果我们输入错误的URL,则Webview在iOS中始终为空白视图。因此无法执行char choice = '1'; int nbrSubAssy = 0; int totalNumber = 0; long arrangement = 0; while (choice != '0') { // code ... switch (choice) { case '0': Environment.Exit(0); break; case '1': totalNumber = UserSelection("Total number of elements to be taken into account"); long permutation = IntMultiplication(1,totalNumber); Console.WriteLine(totalNumber + "! = " + permutation); break; case '2': nbrSubAssy = UserSelection("Total number of elements in the subassy to be taken into account"); arrangement = IntMultiplication(totalNumber - nbrSubAssy + 1,totalNumber); Console.WriteLine("A(" + totalNumber + "/" + nbrSubAssy + ") = " + arrangement); break; case '3': long combination = arrangement / IntMultiplication(1,nbrSubAssy); Console.WriteLine("C(" + totalNumber + "/" + nbrSubAssy + ") = " + combination); break; default: Console.WriteLine("Wrong input"); break; } }

如果我们使用正确的url,则Webview可能会生成NavigatedCommand,并产生如下屏幕截图所示的运行结果。

enter image description here

有没有适当的方法来解决这个问题?

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

NavigatedCommand

如果我输入了错误的URL,则可以执行[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 } }

enter image description here

,

基于以下链接,我发现了另一种似乎可行的方法: 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访问所需的事件。

因此,我创建了一个CustomWebView控件,该控件继承自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());
    }
}

然后我用CustomWkWebViewRenderer自定义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)));
        }
    }
}

我也有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重试和刷新,即使这可能不是更好的解决方案...

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...