Jeremy Thompson
Jeremy Thompson

Reputation: 65554

Calling a web service via an Async Task from UI thread is causing a deadlock

I am a newbie to Xamarin Forms and have an App that fetch's Transactions asynchronously but it encounters a Deadlock when I call a web service.

I have a TransactionView:

enter image description here

This is the XAML:

<Label Text="Account:" Grid.Row="0" Grid.Column="0" Style="{StaticResource LabelStyle}"/> 
<ctrls:BindablePicker x:Name="ddlAccountsWinPhone" ItemsSource="{Binding Accounts}" SelectedIndex="{Binding AccountID}"  Grid.Row="0" Grid.Column="1" />
<Label Text="From Date:" Grid.Row="2" Grid.Column="0" Style="{StaticResource LabelStyle}"/> <DatePicker x:Name="dtFromWinPhone" Date="{Binding FromDate}" Grid.Row="2" Grid.Column="1"  /> 
<Label Text="To Date:" Grid.Row="3" Grid.Column="0" Style="{StaticResource LabelStyle}"/> <DatePicker x:Name="dtToWinPhone" Date="{Binding ToDate}"  Grid.Row="3" Grid.Column="1" /> 
<Button x:Name="btnViewWinPhone" Text="View" Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"  
Command ="{Binding ShowTransactions}" />

This is the TransactionView's code behind, note I'll show a grid of transactions on subpage called TransactionSubPageView:

public partial class TransactionView : ContentPage
{
    private TransactionViewModel transactionViewModel;
    private TransactionViewModel ViewModel
    {
        get {
            if (transactionViewModel == null) transactionViewModel = new TransactionViewModel(
             this.Navigation, new TransactionSubPageView()); //Pass sub page to ViewModel ctor

            return transactionViewModel;
        }
    }

    public TransactionView()
    {
        InitializeComponent();
        BindingContext = ViewModel;
    }

Here is the TransactionViewModel:

public class TransactionViewModel : ViewModelBase
{
    private int accountID;
    private List<Account> accounts = new List<Account>();
    private Account accountItemSelected;
    private DateTime fromDate = new DateTime(1900,1,1);
    private DateTime toDate = new DateTime(1900, 1, 1);

    private ContentPage transactionGridViewNav;

    public TransactionViewModel(INavigation navigationInterface, ContentPage transactionSubPageView)
    {
        base.navigation = navigationInterface;
        transactionSubPageViewNav = transactionSubPageView; //I SAVE A REFERENCE TO THE SUBPAGE HERE
        accounts.AddRange(Client.Accounts);
    }

    public ICommand ShowTransactions
    {
        get
        {
            return new Command(async () =>
            { 
                //HERE IS WHERE "I THINK" I WANT TO FETCH THE TRANSACTIONS

                //THEN NAVIGATE TO THE THE SUBPAGE WITH THE GRID OF TRANSACTIONS
                await navigation.PushAsync(transactionSubPageViewNav);
            });
        }
    }

    public int AccountID
    {
        get{ return this.accountID; }
        set{
            this.accountID = value;
            this.OnPropertyChanged("AccountID");
            }
    }

    public List<Account> Accounts
    {
        get{
            return this.accounts;}
        set{
            this.accounts = value;
            this.OnPropertyChanged("Accounts");
            }
    }

    public Account AccountItemSelected
    {
        get{return accountItemSelected;}
        set {
                if (accountItemSelected != value)
                {
                    accountItemSelected = value;
                    OnPropertyChanged("AccountItemSelected");
                }
            }
    }

    public DateTime FromDate { get; set; }
    public DateTime ToDate { get; set; }}
    ...

This is the TransactionSubPageView:

public partial class TransactionSubPageView : ContentPage
{
    public TransactionSubPageViewModel transactionSubPageViewModel;

    public TransactionSubPageViewModel ViewModel
    {
        get
        {
            if (transactionSubPageViewModel == null)
                transactionSubPageViewModel = new TransactionSubPageViewModel();
            return transactionGridViewModel;
        }
    }

    private Grid gridTransactions;

    public TransactionSubPageView()
    {
        InitializeComponent();
        BindingContext = ViewModel;
    }

    protected async override void OnAppearing()
    {
        base.OnAppearing();
        //THIS IS A VOID TO POPULATE A GRID AND SET THE PAGE'S CONTENT, IT USES 
        //transactionSubPageViewModel.TransactionsGrid!!
        PopulateGridTransactions();
    }

This is the SubPage ViewModel:

public class TransactionSubPageViewModel : ViewModelBase
{
    public List<Transaction> transactionsGrid = new List<Transaction>();

    public int accountId = 1636;
    public DateTime fromDate = new DateTime(2015, 8, 1);
    public DateTime toDate = new DateTime(2015, 9, 1);

    public TransactionGridViewModel() { }

    public List<Transaction> TransactionsGrid
    {
        get {
            if (transactionsGrid == null) transactionsGrid = MyWebService.GetTransactions(1636, new DateTime(2015, 8, 1), new DateTime(2015, 9, 1)).Result;
            return transactionsGrid;}
    }
}

Lastly here is the WebService Call which is causing the problem:

public static async Task<List<Transaction>> GetTransactions(int accountId, DateTime fromDate, DateTime toDate)
{
    var client = new System.Net.Http.HttpClient(new NativeMessageHandler());
    client.BaseAddress = new Uri("http://theWebAddress.com/);
    var response = await client.GetAsync("API.svc/Transactions/" + accountId + "/" + fromDate.ToString("yyyy-MM-dd") + "/" + toDate.ToString("yyyy-MM-dd")); //.ConfigureAwait(false);
    var transactionJson = response.Content.ReadAsStringAsync().Result;
    var transactions = JsonConvert.DeserializeObject<List<Transaction>>(transactionJson);
    return transactions;
}

Thanks for reading so far, the problem is this line in the webmethod call always hangs:

var response = await client.GetAsync("API.svc/Transactions/" + accountId + "/" + fromDate.ToString("yyyy-MM-dd") + "/" + toDate.ToString("yyyy-MM-dd")); 

If I call the GetTransactions webservice from the SubPage's OnAppearing event it hangs, if I call it from ICommand ShowTransactions it also hangs. Am I missing an await or a continue?

I've read a fair few documents with similar people and who are confused, I know I am encountering a deadlock but I just don't know how to fix it.

I've tried ConfigureAwait(false) without luck. It would be ideal if I could just call the WebService on a background thread, show a progress bar and when the operation is complete render the results in the page.

Upvotes: 1

Views: 1163

Answers (1)

Jeremy Thompson
Jeremy Thompson

Reputation: 65554

Success!! I knew this would be a case of asking here and then I would work it out. Funny how that works!

This is how I got it working feel free to critique it:


I turned the TransactionSubPageViewModel's TransactionGrid property into an Async method and put the webservice call in there directly:

public async Task<List<Transaction>> TransactionsGrid()
{
    if (transactionsGrid == null)
    {
        var client = new System.Net.Http.HttpClient(new ModernHttpClient.NativeMessageHandler());
        client.BaseAddress = new Uri("http://theWebAddress.com/);
        var response = await client.GetAsync("API.svc/Transactions/" + accountId + "/" + fromDate.ToString("yyyy-MM-dd") + "/" + toDate.ToString("yyyy-MM-dd"));
        var transactionJson = await response.Content.ReadAsStringAsync(); //DONT USE Result HERE, USE AWAIT AS PER @Noseratio suggested
        var transactions = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Transaction>>(transactionJson);
        transactionsGrid = transactions;
    }
        return transactionsGrid;
}

Then I called the web service from where I thought I wanted to call it.

public ICommand ShowTransactions
{
    get
    {
        return new Command(async () =>
        { 
            //THIS IS WHERE I WAS MISSING THE AWAIT!!!
            await ((TransactionGridViewModel)transactionSubPageViewNav.BindingContext).TransactionsGrid();

            await navigation.PushAsync(transactionSubPageViewNav);
        });
    }

Then when I call the PopulateGridTransactions from the OnAppearing() the data is already available:

public void PopulateGridTransactions()
{
...
  foreach (Models.Transaction transaction in transactionSubPageViewModel.TransactionsGrid().Result)
  {

Edit:

As @Noseratio points out, let me explain why you need a Try/Catch in the async.

Coincidentally after getting a response from the webservice I got an error on the next line when I deserialized the json web service result.

enter image description here

I'd setup a global exception handler in the Xamarin App.cs so it was catching it:

enter image description here

By putting a Try/Catch in the async I can catch the exception on the same frame (before the stack has been unwound when it is caught by the Application_UnhandledException - which is very annoying to track down ):

enter image description here

Upvotes: 1

Related Questions