1

I am very new to c# so this could be very easy. I am busy creating a xamarin forms app and need the data for the next page on the UI. Its a tabbeb page and all 3 needs the info that will be returned by the base class. The problem I am having is that I need to call google api service for company info. Due to this I created a asynchronous call. So now the code returns due to this. Due to the tabbed page I need the data to bind to the screen and I now need to wait for the data.So basically need it to be synchronous.

I have tried everything I could find on this topic. Maybe the way I am doing this is wrong but hopefully my code will show that.

This is the Tabbed Page:

    public BusinessTabbedPage(string PlaceId)
    {
        Children.Add(new BusinessSpecialsPage(PlaceId));
        InitializeComponent();
    }

This will be one of the pages on the app that calls the viewmodel

    public BusinessSpecialsPage(string PlaceId)
    {
        BindingContext = new BusinessSpecialsPageViewModel(PlaceId);
        InitializeComponent();
    }

Due the 3 pages needing the same data I created a base class. This will get the data and pass everything back to the UI.

    public BusinessBaseViewModel(string placeId)
    {
        Task<List<CompanyProfileModel>> task = GBDFromGoogle(placeId);
        task.Wait();
    }

    public async Task<List<CompanyProfileModel>> GBDFromGoogle(string PlaceId)
    {
        var info = await ApiGoogle.GetGoogleCompanySelectedDetails(PlaceId);
        var Companyresult = info.result;

        CompanyProfileModel CompList = new CompanyProfileModel
        {
            ContactDetails = Companyresult.formatted_phone_number,
            Name = Companyresult.name,
            Website = Companyresult.website,
        };
        ComPF.Add(CompList);

        return ComPF;
    }

This is the api call which i think is adding a new task and then the process deadlocks?

    public static async Task<GoogleSelectedPlaceDetails> GGCSD(string place_id)
    {
        GoogleSelectedPlaceDetails results = null;

        var client = new HttpClient();
        var passcall = "https://maps.googleapis.com/maps/api/place/details/json?placeid=" + place_id + "&key=" + Constants.GoogleApiKey;
        var json = await client.GetStringAsync(passcall);
        //results = await Task.Run(() => JsonConvert.DeserializeObject<GoogleSelectedPlaceDetails>(json)).ConfigureAwait(false);
        results = JsonConvert.DeserializeObject<GoogleSelectedPlaceDetails>(json);

        return results;
    }

I need the process not to deadlock. It needs to wait for the tasks to be done so that I can return the data to the screen.

Roean
  • 13
  • 3
  • 3
    You shouldn't be calling async code inside a constructor, instead do that in an initialisation method that you can properly await. – DavidG Sep 14 '19 at 10:48
  • Thanks but how will I then call the async method. Please can you provide an example – Roean Sep 14 '19 at 11:07
  • 1
    Please see https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html and https://stackoverflow.com/q/23048285/11683. – GSerg Sep 14 '19 at 11:52

1 Answers1

1

Task.Wait will block the current thread so you should never use it in Xamarin applications and even less in the constructor, otherwise your application will freeze until you receive some data which might never happen if the service is down or the user loses connection.

Instead of calling the data in your ViewModel's constructor you could initialize it on the Appearing event of your ContentPage view.

To do that, you could create a custom behaviour or even better, you can use the following library which already does this for you:

Using NuGet: Install-Package Behaviors.Forms -Version 1.4.0

Once you have installed the library, you can use the EventHandlerBehavior to wire events to ViewModel commands, for example:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:behaviors="clr-namespace:Behaviors;assembly=Behaviors"
             xmlns:viewModels="clr-namespace:YourApp.ViewModels"
             Title="Business Page"
             x:Class="YourApp.Views.BusinessPage">

    <ContentPage.BindingContext>
        <viewModels:BusinessViewModel />
    </ContentPage.BindingContext>

    <ContentPage.Behaviors>
        <behaviors:EventHandlerBehavior EventName="Appearing">
            <behaviors:InvokeCommandAction Command="{Binding AppearingCommand}" />
        </behaviors:EventHandlerBehavior>
    </ContentPage.Behaviors>

    [...]

The ViewModel:

public BusinessBaseViewModel(string placeId)
{
    AppearingCommand = new Command(Appearing);
    PlaceId = placeId;
}

public ICommand AppearingCommand { get; private set; }

public string PlaceId { get; private set; }

private ObservableCollection<CompanyProfileModel> _googleGbd;
public ObservableCollection GoogleGbd
{
    get { _googleGbd?? (_googleGbd = new ObservableCollection<CompanyProfileModel>()); };
    set 
    {
         if (_googleGdb != value)
         {
             _googleGdb = value;
             NotifyPropertyChanged();
         }
    }
}

private async void Appearing()
{
    var companyResult = await ApiGoogle.GetGoogleCompanySelectedDetails(PlaceId);

    CompanyProfileModel companyProfile = new CompanyProfileModel
    {
        ContactDetails = companyResult.formatted_phone_number,
        Name = companyResult.name,
        Website = companyResult.website,
    };
    GoogleGbd.Add(companyProfile);
}

If you only want the data to be loaded the first time your View appears, then you can add a bool flag to know that you have already loaded the data.

Isma
  • 13,156
  • 5
  • 30
  • 45