InvalidCastException when binding a Xamarin ListView to an IEnumerable<Person> - c#

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!

Related

Xamarin Geocoding Placemark Exception

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

C# WPF ComboBox ItemsSource Binding Issue

I have an issue with binding ComboBox ItemsSource with a list. I read workplaces from csv file. It can't see the Workplaces list. Please, tell me what is wrong.
xaml:
<ComboBox Grid.Column="1" Grid.Row="9" Height="25" Margin="0,18,0,0" ItemsSource="{Binding Workplaces}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding title}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
xaml.cs:
public partial class MainWindow : Window
{
private BindableCollection<WorkplaceInfo> Workplaces { get; set; }
public MainWindow()
{
InitializeComponent();
Workplaces = new BindableCollection<WorkplaceInfo>(GetWorkplaces());
}
private List<WorkplaceInfo> GetWorkplaces()
{
List<WorkplaceInfo> workplaces = new List<WorkplaceInfo>();
using (var streamReader = new StreamReader("Workplaces.csv"))
{
using (var csvReader = new CsvReader(streamReader, CultureInfo.InvariantCulture))
{
//csvReader.Context.RegisterClassMap<WorkplaceInfoClassMap>();
var workplaceInfoList = csvReader.GetRecords<dynamic>().ToList();
foreach (var wi in workplaceInfoList)
{
workplaces.Add(new WorkplaceInfo(wi.title, wi.member_of.Split(";")));
}
}
}
return workplaces;
}
}
WorkplaceInfo class:
class WorkplaceInfo
{
public String title { get; }
public String[] memberOfList { get; }
public WorkplaceInfo(string title, string[] memberOfList)
{
this.title = title;
this.memberOfList = memberOfList;
}
}
Here is your code optimized to work:
public ObservableCollection<WorkplaceInfo> Workplaces { get; set; }
public MainWindow()
{
this.DataContext = this;
Workplaces = new ObservableCollection<WorkplaceInfo>(GetWorkplaces());
InitializeComponent();
}
private List<WorkplaceInfo> GetWorkplaces()
{
List<WorkplaceInfo> workplaces = new List<WorkplaceInfo>();
try
{
using (var streamReader = new StreamReader("Workplaces.csv"))
{
using (var csvReader = new CsvReader(streamReader, CultureInfo.CurrentCulture))
{
//csvReader.Context.RegisterClassMap<WorkplaceInfoClassMap>();
var workplaceInfoList = csvReader.GetRecords<dynamic>().ToList();
foreach (var wi in workplaceInfoList)
{
var titl = wi.title;
workplaces.Add(new WorkplaceInfo(wi.title, new List<string>() { wi.member_of }.ToArray()));
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
return workplaces;
}
So the changes needed in your code are:
Set your Workplaces collection to public ObservableCollection
Add DataContext binding
Read in the Date and create the collection before the main window is initialized (other way round the UI will not detect the change in you object unless you implement INotifyPropertyChanged event)
p.s. I don't know the structure of your csv file so I made my small demo like (Workplaces.csv) and adopted the parser. You can keep your parser if it matches your csv file structrue.:
title;member_of
London;First
Amsterdam;Second
And my warm recommendation is to use try-catch block always when handling files and when working with anything what is external to your application.
Best regards.

C# Xamarin Forms Populating CollectionView from ViewModel is always null

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;

Get ListView item's details on ItemClick() event

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

combobox splitting values up to 1 char and only getting the first value from json

i am trying to populate my combobox with a json string with around 30 values
but it only takes the first value (4x98) and splitting it up so it is
4
x
9
8
private void bindkrydsmål()
{
{
try
{
string Url = URL_Domain + "resources/bolt-pattern";
Uri serviceUri = new Uri(Url);
using (WebClient webClient = new WebClient())
{
webClient.Encoding = Encoding.UTF8;
string api = webClient.DownloadString(serviceUri);
List<boltPatterns> values = JsonConvert.DeserializeObject<List<boltPatterns>>(api);
comboBox_Copy.DataContext = values;
}
}
catch (Exception es)
{
}
}
}
public class boltPatterns
{
public string BoltPattern { get; set; }
}
combobox xaml
<ComboBox x:Name="comboBox_Copy" ItemsSource="{Binding Path=BoltPattern}" Width="150" Height="40" Foreground="#FF00FB0B" Background="#FF303030" FontSize="16" Canvas.Left="1030" Canvas.Top="24" Style="{StaticResource ComboBoxTest2}">
api value
"[{\"BoltPattern\":\"4x98\"},{\"BoltPattern\":\"5x108\"},{\"BoltPattern\":\"5x114.3\"},{\"BoltPattern\":\"6x180\"},{\"BoltPattern\":\"4x100\"},{\"BoltPattern\":\"8x165.1\"},{\"BoltPattern\":\"5x100\"},{\"BoltPattern\":\"5x165\"},{\"BoltPattern\":\"5x120.65\"},{\"BoltPattern\":\"6x115\"},{\"BoltPattern\":\"6x127\"},{\"BoltPattern\":\"5x118\"},{\"BoltPattern\":\"5x150\"},{\"BoltPattern\":\"5x127\"},{\"BoltPattern\":null}]"
Its just need a property name to bind. Rest all is fine
DisplayMemberPath="BoltPattern"
and
comboBox_Copy.ItemsSource= values;

Categories

Resources