【问题标题】:WebView and error management, what's wrong?WebView 和错误管理,有什么问题?
【发布时间】:2021-02-13 07:22:03
【问题描述】:

我正在开发一个 Xamarin.Forms 应用,我需要在其中集成 WebView 来管理通过外部 URL 进行的预订。

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

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

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

为此,我使用 EventToCommandBehavior 访问 ViewModel 中的事件 Navigating 和 Navigating。

所以我的 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.Failure 的情况在 WebViewNavigatedAsync() 中被捕获,但我没有'不要在 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()

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

【问题讨论】:

    标签: c# xamarin.forms error-handling webview connectivity


    【解决方案1】:

    这正常吗?

    根据我的测试。是的,我得到了同样的结果,如果我们输入一个错误的 url,webview 在 iOS 中总是白空视图。所以NavigatedCommand不能被执行。

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

    有没有合适的方法来管理这个?

    我们可以在 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 可能会被执行。

    【讨论】:

    • 感谢您的反馈@Leon Lu - MSFT 是否也可以管理其他错误? (超时,不正确的响应状态,...)这也意味着我必须显示来自渲染器的错误弹出窗口?在这种情况下,可能无法显示带有“重试”按钮的错误视图...
    • @Gold.strike navtive IOS 可以处理以下错误。docs.microsoft.com/en-us/dotnet/api/…
    • 难道不能在 ViewModel 中使用通用的 iOS/Android 代码来识别错误(错误的 URL、超时、没有互联网……)并管理它们(全屏错误通过在`LoadFailed()`中捕获错误后立即触发WebView.Nagitated 事件,使用“重试”按钮进行查看,弹出)?
    • 您可以使用MessageCenter来通知webview加载错误,您可以在LoadFailed()中发送消息,然后在PCL中订阅消息。
    【解决方案2】:

    根据以下链接,我发现了另一种似乎可行的方法:

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

    所以我创建了一个继承自WebView 的CustomWebView 控件:

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

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

        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 刷新,即使它可能不是更好的解决方案...

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-08-11
      • 2022-01-04
      • 1970-01-01
      • 1970-01-01
      • 2011-09-03
      • 2012-02-13
      • 1970-01-01
      • 2023-03-19
      相关资源
      最近更新 更多