Xamarin 表单 HTTPClient 调用崩溃

Xamarin Form HTTPClient Call crashing

我有一个正在使用 System.Net.Http.HttpClient 的项目。我正在尝试集中对我的 Web API 的所有调用,以便我有常见的错误处理等。我在我的项目中创建了以下 class。

using ModernHttpClient;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace WebAPIHelper
{
    class WebAPICaller
    {
        public async Task<string> CallWebService(string ps_URI)
        {
            HttpClient lobj_HTTPClient = null;
            HttpResponseMessage lobj_HTTPResponse = null;
            string ls_Response = "";

            //We assume the internet is available. 
            try
            {
                //Get the Days of the Week
                lobj_HTTPClient = new HttpClient(new NativeMessageHandler());
                lobj_HTTPClient.BaseAddress = new Uri(App.APIPrefix);
                lobj_HTTPClient.DefaultRequestHeaders.Accept.Clear();
                lobj_HTTPClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                lobj_HTTPResponse = await lobj_HTTPClient.GetAsync(ps_URI);

                if (!lobj_HTTPResponse.IsSuccessStatusCode)
                {
                    Debug.WriteLine(lobj_HTTPResponse.ReasonPhrase);
                }
                else
                {
                    ls_Response = await lobj_HTTPResponse.Content.ReadAsStringAsync();
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
            finally
            {
                if (lobj_HTTPClient != null)
                    lobj_HTTPClient.Dispose();
                if (lobj_HTTPResponse != null)
                {
                    lobj_HTTPResponse.Dispose();
                }
            }

            return ls_Response;

        }

    }
}

我从我在 ViewModel class 中创建的实例对象调用函数,如下所示:

using ModernHttpClient;
using Newtonsoft.Json;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace WebAPIHelper
{
    public class VM_Languages : INotifyPropertyChanged
    {
        /// <summary>
        /// A collection for CGSLanguage objects.
        /// </summary>
        public ObservableCollection<GBSLanguage_ForList> Items_ForList { get; private set; }
        const string ic_LanguagesAPIUrl = @"/languages/true";

        /// <summary>
        /// Constructor for the Languages view model.
        /// </summary>
        public VM_Languages()
        {
            this.Items_ForList = new ObservableCollection<GBSLanguage_ForList>();
        }

        /// <summary>
        /// Indicates of the view model data has been loaded
        /// </summary>
        public bool IsDataLoaded
        {
            get;
            private set;
        }

        /// <summary>
        /// Creates and adds a the countries to the collection.
        /// </summary>
        public async Task LoadData()
        {
            HttpClient lobj_HTTPClient = null;
            HttpResponseMessage lobj_HTTPResponse = null;
            string ls_Response = "";

            try
            {
                IsDataLoaded = false;
                string ls_WorkLanguageURI = ic_LanguagesAPIUrl;

                //Get the Days of the Week
                lobj_HTTPClient = new HttpClient(new NativeMessageHandler());
                lobj_HTTPClient.BaseAddress = new Uri(App.APIPrefix);
                lobj_HTTPClient.DefaultRequestHeaders.Accept.Clear();
                lobj_HTTPClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                **//This call will not work
                //WebAPICaller lobj_APICaller = new WebAPICaller();
                //ls_Response = lobj_APICaller.CallWebService(ls_WorkLanguageURI).Result;

                //This call will work
                lobj_HTTPResponse = await lobj_HTTPClient.GetAsync(ls_WorkLanguageURI);**


                if (lobj_HTTPResponse.IsSuccessStatusCode)
                {

                    if (this.Items_ForList != null)
                        this.Items_ForList.Clear();

                    ls_Response = await lobj_HTTPResponse.Content.ReadAsStringAsync();
                    Items_ForList = JsonConvert.DeserializeObject<ObservableCollection<GBSLanguage_ForList>>(ls_Response);

                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
            finally
            {
                this.IsDataLoaded = true;
                NotifyPropertyChanged("GBSLanguages_ForList");
            }
        }

        /// <summary>
        /// Notifies subscribers that a property has changed. 
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (null != handler)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

我的所有 ViewModel 都在一个静态 class 中,所以我只得到一个实例,如下所示:

namespace WebAPIHelper
{
    public static class ViewModelObjects
    {
        private static VM_Languages iobj_Languages;

        public static VM_Languages Languages
        {
            get
            {
                if (iobj_Languages == null)
                    iobj_Languages = new VM_Languages();
                return iobj_Languages;
            }

        }
    }
}

在我的主页出现的代码中,我有以下调用来从 WebAPI 检索数据

    protected override async void OnAppearing()
    {
        Device.BeginInvokeOnMainThread(() =>
        {
            if (!ViewModelObjects.Languages.IsDataLoaded)
                ViewModelObjects.Languages.LoadData();

        });

        //If the DOW and Language data are not loaded yet - wait
        while (!ViewModelObjects.Languages.IsDataLoaded)
        {
            await Task.Delay(1000);
        }

    }

问题是当我尝试使用我的 WebAPICaller class 时,它似乎崩溃了。我从来没有从中得到 return 。没有出现异常,我的程序也没有继续。

lobj_HTTPResponse = await lobj_HTTPClient.GetAsync(ps_URI);

如果我从我的 ViewModel 发出我认为完全相同的调用,它就会起作用。 (我在视图模型中调用了 WebAPICaller class 并直接调用了 GetAsync,因此您可以通过评论和取消评论来测试它。)

知道我做错了什么吗?

Link 到完整示例项目: https://1drv.ms/u/s!Ams6cZUzaeQy3M8uGAuaGggMt0Fi-A

我看到了一些正在发生的事情。一、commented-out代码:

            //This call will not work
            //WebAPICaller lobj_APICaller = new WebAPICaller();
            //ls_Response = lobj_APICaller.CallWebService(ls_WorkLanguageURI).Result;

正在使用 .Result 而不是 await。这可能是您问题的根源。通常应避免使用 .Result,只需使用 await。有关原因的更多详细信息,请参阅此处 Await on a completed task same as task.Result? 的答案。

其次是调用OnAppearing时你已经在UI线程上了,所以没必要调用Device.BeginInvokeOnMainThread。您当前在该方法中所做的相当于:

protected override async void OnAppearing()
{
    if (!ViewModelObjects.Languages.IsDataLoaded)
        await ViewModelObjects.Languages.LoadData();
}

下一个问题是在 OnAppearing() 中执行此操作是否是个好主意。这可能会导致您的应用看起来 non-responsive.

Device.BeginInvokeOnMainThread 的一般用途是当你不知道你当前是否在主线程上时,但是这些 UI 事件处理程序总是在主线程上被调用Xamarin 平台。

这就是我的发现。似乎等待 HTTPClient.GetAsync 导致了错误。 (很确定线程被阻塞了。)所以首先我取出等待并添加代码以在 HTTPClient 的 return 任务未完成时延迟任务。

var lobj_Result = lobj_HTTPClient.GetAsync(ps_URI);

while (!lobj_Result.IsCompleted)
{
    Task.Delay(100);
}

因为我不再等待 LoadData 方法中的调用,所以我能够删除 async Task 声明并简单地使其成为一个方法。

    public void LoadData()
    {
        HttpClient lobj_HTTPClient = null;
        HttpResponseMessage lobj_HTTPResponse = null;
        string ls_Response = "";

        try
        {
            IsDataLoaded = false;
            string ls_WorkLanguageURI = ic_LanguagesAPIUrl;

            //Get the Days of the Week
            lobj_HTTPClient = new HttpClient(new NativeMessageHandler());
            lobj_HTTPClient.BaseAddress = new Uri(App.APIPrefix);
            lobj_HTTPClient.DefaultRequestHeaders.Accept.Clear();
            lobj_HTTPClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            //This call will not work
            WebAPICaller lobj_APICaller = new WebAPICaller();
            ls_Response = lobj_APICaller.CallWebService(ls_WorkLanguageURI).Result;

            if (ls_Response.Trim().Length > 0)
            {
                if (this.Items_ForList != null)
                    this.Items_ForList.Clear();
                Items_ForList = JsonConvert.DeserializeObject<ObservableCollection<GBSLanguage_ForList>>(ls_Response);

            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
        finally
        {
            this.IsDataLoaded = true;
            NotifyPropertyChanged("GBSLanguages_ForList");
        }
    }

我可以从出现的事件中删除异步并简单地同步调用 loaddata 事件。

   if (!ViewModelObjects.Languages.IsDataLoaded)
                ViewModelObjects.Languages.LoadData();

进行所有这些更改后,达到了预期的结果。一切都以同步方式 运行(我知道对 HTTPClient 的 GetAsync 函数的调用仍然是异步的)并且在结果被检索并加载到对象中之前不会 return。

我希望这可以帮助其他人。 :)

基于 source code on github

void IPageController.SendAppearing()
{
    if (_hasAppeared)
        return;

    _hasAppeared = true;

    if (IsBusy)
        MessagingCenter.Send(this, BusySetSignalName, true);

    OnAppearing();
    EventHandler handler = Appearing;
    if (handler != null)
        handler(this, EventArgs.Empty);

    var pageContainer = this as IPageContainer<Page>;
    ((IPageController)pageContainer?.CurrentPage)?.SendAppearing();
}

还有一种方法可以用 async/await 方法做到这一点。

您会注意到 OnAppearing 方法在事件触发之前被调用。

订阅 page/view

Appearing 活动
protected override void OnAppearing() {
    this.Appearing += Page_Appearing;
}

并像最初那样创建一个异步方法,但这次将它放在实际的偶数处理程序上

private async void Page_Appearing(object sender, EventArgs e) {
    if (!ViewModelObjects.Languages.IsDataLoaded)
        await ViewModelObjects.Languages.LoadData();
    //unsubscribing from the event
    this.Appearing -= Page_Appearing;
}

这样在等待任务完成时就不需要忙等待延迟线程了。