2

I have a project where I am using System.Net.Http.HttpClient. I am trying to centralize all calls to my Web APIs so that I have common error handing etc. I created the following class in my project.

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;

        }

    }
}

I call the function from an instance object I created in my ViewModel class for languages as follows:

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

I have all my ViewModels in a static class so I only ever get one instance of them as follows:

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

        }
    }
}

In my on appearing code of my main page I have the following call to retrieve the data from the 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);
        }

    }

The problem is when I attempt to use my WebAPICaller class, it appears to crash on the line. I never get a return from it. No exceptions are ever raised and my program never continues.

lobj_HTTPResponse = await lobj_HTTPClient.GetAsync(ps_URI);

If I make what I believe to be the exact same call from my ViewModel, it works. (I have both the call to the WebAPICaller class as well as a direct call to GetAsync in the View Model so you can test it out by commenting and uncommenting.)

Any idea as to what I am doing wrong?

Link to full sample project: https://1drv.ms/u/s!Ams6cZUzaeQy3M8uGAuaGggMt0Fi-A

Nkosi
  • 235,767
  • 35
  • 427
  • 472
George M Ceaser Jr
  • 1,497
  • 3
  • 23
  • 52
  • 2
    use Fiddler or a similar tool to see what request is being made to the API. This sounds more like a debugging issue... – CodingYoshi Jan 22 '17 at 21:24
  • This is because you are mixing async and blocking calls. Unless `OnAppearing` is an event handler the async calls within that method will deadlock. – Nkosi Jan 22 '17 at 21:36
  • On appearing is actually an event handler from the page. What you are describing is kind of what I thought but don't know how to fix it – George M Ceaser Jr Jan 22 '17 at 22:36
  • @GeorgeMCeaserJr, `OnAppearing` is not an event handler. It is a protected method called within the actual event handler for `Page.Appearing` event. – Nkosi Jan 23 '17 at 00:48
  • @GeorgeMCeaserJr, If you look at the [source code on github](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/Page.cs#L295) You will see what I am referring to. – Nkosi Jan 23 '17 at 00:57
  • @GeorgeMCeaserJr check my answer and see if this approach will work for you. – Nkosi Jan 23 '17 at 16:39

3 Answers3

1

So here is what I found. It seems that awaiting the HTTPClient.GetAsync was causing the error. (Pretty sure the thread was blocked.) So to start with I took out the await and added code to delay the task when HTTPClient's return task was not completed.

var lobj_Result = lobj_HTTPClient.GetAsync(ps_URI);

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

Because I no longer await the call in the LoadData method, I was able to remove the async Task declaration and simply make it a method.

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

The I could remove the async from my on appearing event and simply call the loaddata event synchronously.

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

After making all these changes, the desired result was achieved. Everything ran in synchronous manner (I know the call to GetAsync function of the HTTPClient is still async) and did not return until the results were retrieve and loaded into the object.

I hope this helps other people out. :)

George M Ceaser Jr
  • 1,497
  • 3
  • 23
  • 52
0

There are a couple of things I see going on. First, the commented-out code:

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

Is using .Result instead of await. This may be the root of your problem. Using .Result should be avoided in general, just use await. See the answer here Await on a completed task same as task.Result? for more details on why.

The second thing is that you are already on the UI thread when OnAppearing is called, so there is no need to call Device.BeginInvokeOnMainThread. What you're doing in that method currently is equivalent to:

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

The next question is whether or not it's a great idea to be doing this in OnAppearing(). This can cause your app to seem non-responsive.

The general use of Device.BeginInvokeOnMainThread is for when you don't know if you're currently on the main thread, but these UI event handlers always are called on the main thread by the Xamarin platform.

Community
  • 1
  • 1
DavidS
  • 2,904
  • 1
  • 15
  • 21
  • .Result is not the problem as I am never even getting to that line of code. The Await and another Await seemed to be the challenge. I think I fixed it and will post an answer in a few minutes after I test it out a little more. – George M Ceaser Jr Jan 23 '17 at 14:28
0

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

there is still a way to do this with async/await approach.

You will notice that the OnAppearing method is called just before the event is triggered.

Subscribe to the Appearing event of the page/view

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

and create an async method like you did originally but this time have it on an actual even handler

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

This way there is no need to busy wait delay the thread while waiting for the task to complete.

Nkosi
  • 235,767
  • 35
  • 427
  • 472