Reputation: 65554
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:
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
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.
I'd setup a global exception handler in the Xamarin App.cs so it was catching it:
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 ):
Upvotes: 1