I am creating a project where getting the users' address are involve. I tried using the Xamarin.Essentials Geolocation (to get the coordinates) and Geocoding (to get the placemarks via coordinates). I am successful at getting the coordinates through geolocation (returns Location) but I get an exception on getting the placemark. I can't seem to find the main source of the problem in the internet.
Here is the Exception:
**Java.IO.IOException:** 'doqz: DEADLINE_EXCEEDED: deadline exceeded after 4.925084576s. [closed=[], open=[[buffered_nanos=4926319970, waiting_for_connection]]]'
Here is my code below. (I simplified it to only get the placemark from a given coordinate and still produce the same exception on my end):
LocationServices.cs
public class LocationServices
{
private static LocationServices _LocationClientInstance;
public static LocationServices LocationClientInstance
{
get
{
if (_LocationClientInstance == null)
_LocationClientInstance = new LocationServices();
return _LocationClientInstance;
}
}
public async Task<Placemark> getPlacemark(Location coordinates)
{
Placemark result = null;
try
{
var placemarks = await Geocoding.GetPlacemarksAsync(coordinates.Latitude, coordinates.Longitude);
result = placemarks?.FirstOrDefault();
}
catch (Exception e)
{
await App.Current.MainPage.DisplayAlert("Cant Get Placemark", $"Cant Get Placemark\n{e.Message}", "OK");
}
return result;
}
}
ViewModel.cs
public class ViewModel: BaseViewModel
{
public ViewModel()
{
getLocCom = new AsyncCommand(getPlacemarkTry);
}
private string _loc;
public string loc
{
get => _loc;
set => SetProperty(ref _loc, value);
}
public ICommand getLocCom { get; }
private async Task getPlacemarkTry()
{
Location current = new Location(14.58300883983523, 121.11321186835512);
var result = await LocationServices.LocationClientInstance.getPlacemark(current);
loc = await LocationServices.LocationClientInstance.getLocation(result);
}
}
View
<StackLayout>
<Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
<Label Text="Location Sample" HorizontalTextAlignment="Center" TextColor="White" FontSize="36"/>
</Frame>
<Button Text="Get Location" Command="{Binding getLocCom}"/>
<Label Text="{Binding loc}"/>
</StackLayout>
I have a feeling that it only shows on my end. Please pitch in some of your suggestions and solution regarding this problem. Thank you
Related
I am trying to populate a collection view from a ViewModel, however when I try to bind the data to the collection view, the ViewModel is null.
xaml.cs file
ObservableCollection<ReportsClass> newKidList = new ObservableCollection<ReportsClass>();
public ReportsViewModel viewmodel { get; set; }
public ReportsPage()
{
InitializeComponent();
viewmodel = new ReportsViewModel();
this.BindingContext = viewmodel;
PreviousDateRange.CornerRadius = 20;
NextDateRange.CornerRadius = 20;
DateTime firstDate = currentDate.StartOfWeek(DayOfWeek.Sunday);
DateTime secondDate = currentDate.AddDays(7).StartOfWeek(DayOfWeek.Saturday);
DateRange.Text = firstDate.ToString("MMMM d") + " - " + secondDate.ToString("MMMM d");
Kids.SetBinding(ItemsView.ItemsSourceProperty, nameof(viewmodel.kids));
}
Here is my view model
public class ReportsViewModel
{
public ObservableCollection<ReportsClass> kids { get; set; }
FirebaseStorageHelper firebaseStorageHelper = new FirebaseStorageHelper();
WebServiceClass webServiceClass = new WebServiceClass();
DateTime currentDate = DateTime.Now;
public ReportsViewModel()
{
GetKids();
}
public async void GetKids()
{
var parentId = await SecureStorage.GetAsync("parentid");
kids = await webServiceClass.Reports(Convert.ToInt32(parentId), currentDate.StartOfWeek(DayOfWeek.Sunday), currentDate.AddDays(7).StartOfWeek(DayOfWeek.Saturday));
}
}
And here is the method that gets the data for the view model
public async Task<ObservableCollection<ReportsClass>> Reports(int parentid, DateTime startDate, DateTime endDate)
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("parentid", parentid.ToString()),
new KeyValuePair<string, string>("startDate", startDate.ToString("yyyy-MM-dd H:mm:ss")),
new KeyValuePair<string, string>("endDate", endDate.ToString("yyyy-MM-dd"))
});
var response = await client.PostAsync(string.Format("https://example.com/api/index.php?action=reports"), content);
var responseString = await response.Content.ReadAsStringAsync();
ObservableCollection<ReportsClass> items = JsonConvert.DeserializeObject<ObservableCollection<ReportsClass>>(responseString);
return items;
}
What am I doing wrong? The purpose of me doing it this way is so I can update an item in the collectionview
Here is my ReportsClass
public class ReportsClass
{
public ReportsClass(string firstName)
{
first_name = firstName;
}
public string first_name { get; set; }
}
OPTION A:
Fix the syntax of Kids.SetBinding, to not get null. Refer to the CLASS ReportsViewModel, not to the INSTANCE viewmodel:
Kids.SetBinding(ItemsView.ItemsSourceProperty, nameof(ReportsViewModel.kids));
The kids still won't appear in list. To fix, kids needs OnPropertyChanged:
public ObservableCollection<ItemModel> kids {
get => _kids;
set {
_kids = value;
OnPropertyChanged();
}
}
private ObservableCollection<ItemModel> _kids;
See the other code in Option B. Adapt as desired.
When you need XAML to see a DYNAMIC change, you need OnPropertyChanged. This is an implementation of INotifyPropertyChanged. Add this call to properties (that XAML binds to) of ReportsClass:
// Inheriting from `BindableObject` is one way to obtain OnPropertyChanged method.
public class ReportsClass : Xamarin.Forms.BindableObject
{
public ReportsClass(string firstName)
{
first_name = firstName;
}
public string first_name {
get => _first_name;
set {
_first_name = value;
// This tells XAML there was a change.
// Makes "{Binding first_name}" work dynamically.
OnPropertyChanged();
}
}
private string _first_name;
}
OPTION B:
Didn't find an answer anywhere that does everything correctly, so here is a complete sample, for future reference:
Remove Kids.SetBinding(...). (It can be fixed as shown in OPTION A, but its easier to get it correct in XAML, so below I show it in XAML.)
Bindings from Page to VM. See xaml below.
Create ObservableCollection with setter that does OnPropertyChanged. This informs XAML when the list is ready, so page updates. (This is an implementation of INotifyPropertyChanged, as Jason mentioned.)
Use Device.BeginInvokeOnMainThread(async () to create an async context, that is queued to run after constructor returns. (This fixes the issue Jason mentioned, which is that a constructor isn't an async context, so should not DIRECTLY call an async method such as QueryItemsAsync, or your GetKids.) This is more reliable.
PageWithQueryData.xaml:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TestXFUWP.PageWithQueryData">
<ContentPage.Content>
<StackLayout>
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding Name}" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.EmptyView>
<Grid>
<Label Text="Loading ..." FontSize="24" TextColor="Blue" BackgroundColor="LightBlue" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
</Grid>
</CollectionView.EmptyView>
</CollectionView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
PageWithQueryData.xaml.cs:
public partial class PageWithQueryData : ContentPage
{
public PageWithQueryData()
{
InitializeComponent();
// ... other initialization work here ...
// BUT remove `Kids.Binding(...);` line. See XAML: `ItemsSource="{Binding Items}"`.
BindingContext = new VMWithQueryData();
}
}
VMWithQueryData.cs:
class VMWithQueryData : Xamarin.Forms.BindableObject
{
public VMWithQueryData()
{
// Start an async task to query.
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () => {
await QueryItemsAsync();
});
// Alternative implementation: Start a background task to query.
//QueryItemsInBackground();
}
public ObservableCollection<ItemModel> Items {
get => _items;
set {
_items = value;
OnPropertyChanged();
}
}
private ObservableCollection<ItemModel> _items;
private async Task QueryItemsAsync()
{
var names = new List<string> { "One", "Two", "Three" };
bool queryOneAtATime = false;// true;
if (queryOneAtATime) {
// Show each item as it is available.
Items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
// Simulate slow query - replace with query that returns one item.
await Task.Delay(1000);
Items.Add(new ItemModel(name));
}
} else {
// Load all the items, then show them.
// Simulate slow query - replace with query that returns all data.
await Task.Delay(3000);
var items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
items.Add(new ItemModel(name));
}
Items = items;
}
}
// Alternative implementation, using a background thread.
private void QueryItemsInBackground()
{
Task.Run(() => {
var names = new List<string> { "One", "Two", "Three" };
bool queryOneAtATime = false;// true;
if (queryOneAtATime) {
// Show each item as it is available.
Items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
// Simulate slow query - replace with query that returns one item.
System.Threading.Thread.Sleep(1000);
Items.Add(new ItemModel(name));
}
} else {
// Load all the items, then show them.
// Simulate slow query - replace with query that returns all data.
System.Threading.Thread.Sleep(3000);
var items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
items.Add(new ItemModel(name));
}
Items = items;
}
});
}
}
ItemModel.cs:
public class ItemModel
{
public ItemModel(string name)
{
Name = name;
}
public string Name { get; set; }
}
This also demonstrates <CollectionView.EmptyView> to display a message to user, while the data is being queried.
For completeness, I've included an alternative QueryItemsInBackground, that uses a background thread instead of an async method. Either approach works well.
Notice inheritance from Xamarin.Forms.BindableObject. This is one way to get an implementation of INotifyPropertyChanged. You can use any other MVVM library or technique.
Move this line of code to the end of your constructor
this.BindingContext = viewmodel;
I've tried searching around here and on microsoft docs but I can't find a solution to my specific query, as mainly I've seen posts about how to do things on itemclick rather than retrieve data.
I'm currently using an API, which sends a JSON request that I deserialize into 2 partial classes, where I use a foreach loop to add new items to the ListView. You can see the classes here:
public partial class GameListObject
{
[JsonPropertyName("id")]
public long GameID { get; set; }
[JsonPropertyName("name")]
public string GameName { get; set; }
[JsonPropertyName("release_dates")]
public ObservableCollection<ReleaseDate> ReleaseDates { get; set; }
}
public partial class ReleaseDate
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("human")]
public string Human { get; set; }
}
And the request, deserialization and adding to the ListView here:
//On search box content change
private async void gamehub_search_TextChanged(object sender, TextChangedEventArgs e)
{
var SearchQuery = gamehub_search.Text;
try
{
// Construct the HttpClient and Uri
HttpClient httpClient = new HttpClient();
Uri uri = new Uri("https://api.igdb.com/v4/games");
httpClient.DefaultRequestHeaders.Add("Client-ID", App.GlobalClientidIGDB);
httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + App.GlobalAccessIGDB);
//Debug.WriteLine("Request Headers: ");
// Construct the JSON to post
HttpStringContent content = new HttpStringContent($"search \"{SearchQuery}\"; fields name,release_dates.human;");
Debug.WriteLine("Request Contents: " + content);
// Post the JSON and wait for a response
HttpResponseMessage httpResponseMessage = await httpClient.PostAsync(
uri,
content);
// Make sure the post succeeded, and write out the response
httpResponseMessage.EnsureSuccessStatusCode();
var httpResponseBody = await httpResponseMessage.Content.ReadAsStringAsync();
Debug.WriteLine("Request Response: " + httpResponseBody);
//Deserialise the return output into game id, game name and release date
List<GameListObject> gamelistobjects = JsonSerializer.Deserialize<List<GameListObject>>(httpResponseBody);
ObservableCollection<GameListObject> dataList = new ObservableCollection<GameListObject>(gamelistobjects);
ObservableCollection<GameListObject> GameList = new ObservableCollection<GameListObject>();
foreach (var item in dataList)
{
Debug.WriteLine($"id: {item.GameID}");
Debug.WriteLine($"name: {item.GameName}");
GameListObject add = new GameListObject() { GameID = item.GameID, GameName = item.GameName };
GameList.Add(add);
if (item.ReleaseDates != null)
{
foreach (var date in item.ReleaseDates)
{
Debug.WriteLine($"releaseDate: {date.Human}");
}
}
}
gamehub_list.ItemsSource = GameList;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
Now I have set an gamehub_list_ItemClick method which runs when an item within the ListView is pressed. I would like to retrieve the GameID that's present in that item because I'll need that for another page which the user gets redirected to so that I know what game I must request data for. However, I've tried finding the index of the item and using the member names of the class to retrieve it but I can't seem to get it working.
The ItemClick method currently is:
private void gamehub_list_ItemClick(object sender, ItemClickEventArgs e) //When an item in List View is pressed
{
string clickedItemText = e.ClickedItem.ToString();
Debug.WriteLine("Click Item text: " + clickedItemText);
}
When I tried to get the index of the item, it always returned as -1 and for the current clickedItemText it returns "Click Item text: ReviewR.GameHubs+GameListObject".
My xaml with the ListView:
<ListView x:Name="gamehub_list" SelectionMode="None" IsItemClickEnabled="True" ItemClick="gamehub_list_ItemClick" Margin="30,140,44,30" BorderThickness="5" BorderBrush="Black" Background="Gray" RequestedTheme="Dark" Visibility="Collapsed">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<StackPanel>
<TextBlock Text="{Binding GameID}"></TextBlock>
<TextBlock Text="{Binding GameName}"></TextBlock>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Click Item text: ReviewR.GameHubs+GameListObject
The problem is ClickedItem is object type, if you pass it to string directly, it will return text like you mentioned above.
For this scenario, you need unbox ClickedItem.
var clickedItem = e.ClickedItem as GameListObject
Debug.WriteLine("Click Item text: " + clickedItem.GameID );
I have this ListView in a Xamarin app:
<ListView Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{x:Static local:Person.All}" x:Name="list">
<ListView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
And I'm trying to bind it to an IEnumerable<Person> by setting the ItemsSource property to the new enumerable. (Yes, I know I should create a viewmodel, but I hate MVVM with a passion!) But when there is data in the enumerable, I get this error:
System.InvalidCastException: Specified cast is not valid.
This does not happen immediately after setting ItemsSource; I have a try/catch block containing the set operation, but no exception is caught. Rather it happens as soon as I get out of the method where I'm setting the ItemsSource. There is no stack trace associated with this error; that's all I get apart from some generic "Unhandled Exception" message.
Here is the method where I am setting the ItemsSource:
private void BtnLogIn_Clicked(object sender, EventArgs e)
{
try
{
// log in
var req = WebRequest.CreateHttp($"http://{Configuration.Hostname}/api/login?systemID=1091&login={txtLogin.Text}&password={txtPassword.Text}");
var resp = (HttpWebResponse)req.GetResponse();
var cookiesData = resp.Headers.Get("Set-Cookie");
var regex = new Regex($#"{CookieManager.LoginCookieID}=(.*); expires=.*");
Login.CookieValue = regex.Match(cookiesData).Groups[1].Captures[0].Value;
list.ItemsSource = Person.All; // reload person list
}
catch (Exception ex)
{
DisplayAlert("Error", ex.ToString(), "OK");
}
}
(yes I know putting a password in a URL is a bad idea; this is just a proof of concept!)
And here is the Person class:
public class Person
{
public static IEnumerable<Person> All => GetWebData<IEnumerable<Person>>($"http://{Configuration.Hostname}/api/people", CookieManager.LoginCookieID, Login.CookieValue);
private static T GetWebData<T>(string url, string cookieKey, string cookieValue)
{
try
{
var web = WebRequest.CreateHttp(url);
web.Headers["Set-Cookie"] = $"{cookieKey}={cookieValue}";
var stream = web.GetResponse().GetResponseStream();
var sr = new StreamReader(stream);
T data;
var json = sr.ReadToEnd();
sr.Close();
try
{
data = JsonConvert.DeserializeObject<T>(json);
}
catch
{
data = JsonConvert.DeserializeObject<IEnumerable<T>>(json).Single();
}
return data;
}
catch
{
// could not access data, maybe not logged in yet?
return default(T);
}
}
public string FirstName { get; set; }
public string LastName { get; set; }
public string Name => $"{FirstName} {LastName}";
}
I figured it out - looks like <Label Text="{Binding Name}" /> is not valid as a DataTemplate; I changed the Label to a TextCell and everything works!
Due to architecture design specifications, I have an application that fills its views from ClassLibraries. The application itself behaves like a sort of Integrator.
Now I need to add localization resources and I can successfully achieve it by adding *.resw files but only if the control is declared inside of the Application project.
What I actually need is to being able to share those resources across the ENTIRE SOLUTION somehow.
Then, the point is to being able to translate any control's content of the solution by using localization resources, preferably using the structure explained above.
For example, I have this following view, which fills the TextBlocks' content depending on the selected language:
<ComboBox x:Name="Languages"
ItemsSource="{Binding Languages}"
SelectedItem="{Binding SelectedLanguage, Mode=TwoWay}">
<i:Interaction.Behaviors>
<iCore:EventTriggerBehavior EventName="SelectionChanged">
<iCore:InvokeCommandAction Command="{Binding ChangeLanguage}" />
</iCore:EventTriggerBehavior>
</i:Interaction.Behaviors>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding LanguageName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="{Binding Model.HelloText}" FontSize="50" Foreground="Red"/>
<TextBlock Text="{Binding Model.HowAreYouText}" FontSize="50" Foreground="Red"/>
<BFview:BFView />
</StackPanel>
Where BFView is a view stored in another project (has two dummy textblocks also)
The Model of that view:
public class MainModel : TranslatableStrings
{
private string helloText, howareuText;
public string HelloText
{
get { return this.helloText; }
set { SetProperty(ref this.helloText, value); }
}
public string HowAreYouText
{
get { return this.howareuText; }
set { SetProperty(ref this.howareuText, value); }
}
}
And the base class of the Model is just a contractual class since it has no implementation, but a base type:
public abstract class TranslatableStrings : BindableBase { }
Then, the View data context is the following one:
public class MainViewModel : BaseViewModel
{
private ObservableCollection<MainViewListRscs> languages = new ObservableCollection<MainViewListRscs>();
private ICommand changeLang;
private MainModel model = new MainModel();
public MainViewModel()
{
Languages = new ObservableCollection<MainViewListRscs>()
{
new MainViewListRscs { LanguageCode = "es-ES", LanguageName = "Español" },
new MainViewListRscs { LanguageCode = "en-EN", LanguageName = "English" },
new MainViewListRscs { LanguageCode = "fr-FR", LanguageName = "Français" },
new MainViewListRscs { LanguageCode = "de-DE", LanguageName = "Deutsch" }
};
}
public ICommand ChangeLanguage
{
get { return changeLang = changeLang ?? new DelegateCommand(OnChangeLanguageRequested); }
}
public ObservableCollection<MainViewListRscs> Languages
{
get { return this.languages; }
set
{
this.languages = value;
OnPropertyChanged();
}
}
public MainViewListRscs SelectedLanguage { get; set; }
public MainModel Model
{
get { return this.model; }
set { this.model = value; }
}
private void OnChangeLanguageRequested()
{
Logger.Debug("MAINVIEW", SelectedLanguage.LanguageName + " selected.");
TranslateManager.UpdateStrings<TranslatableStrings>(SelectedLanguage.LanguageCode, this.Model);
}
public override Task OnNavigatedFrom(NavigationEventArgs args)
{
return null;
}
public override Task OnNavigatedTo(NavigationEventArgs args)
{
return null;
}
}
And the TranslateManager:
public class TranslateManager
{
public async static void UpdateStrings<T>(string langCode, T instance) where T : TranslatableStrings
{
//Get all the classes that implement TranslatableStrings
var currentAssembly = instance.GetType().GetTypeInfo().Assembly;
var translatableClasses = currentAssembly.DefinedTypes.Where(type => type.BaseType == typeof(T)).ToList();
//Open RESX file
ResourceLoader resx = ResourceLoader.GetForCurrentView(langCode);
foreach(var Class in translatableClasses)
{
foreach(var property in Class.DeclaredProperties)
{
string value = resx.GetString(property.Name);
var vmProp = instance.GetType().GetTypeInfo().GetDeclaredProperty(property.Name);
vmProp.SetValue(instance, value);
}
}
}
}
I have achieved changing the two TextBlocks of the MainView but not the view in another project. What I would need to do is to get a list of assemblies contained in a solution. I guess that getting just this would make everything work since I'm using a generic implementation.
Any suggestion will be much appreciated.
Thanks!
Your translation files are loaded as resources. So you can access them anywhere, even in other projects by doing something like
private ResourceLoader _resourceLoader = new ResourceLoader();
var someTranslation =_resourceLoader.GetString("your_localization_key");
Wrap this code nicely into a lib so that you can have an easy access to it from everywhere, and there you go !
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.
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 ):