JSON to ObservableCollection to be used on ListView

JooAlmeida.9370JooAlmeida.9370 PTMember
edited April 2015 in Xamarin.Forms

I'm having problems to convert JSON data from an API to use on my ListView.

Drawn Class

public class Drawn
{
public string date { get; set; }
public string numbers { get; set; }
public string stars { get; set; }
}

public class RootObject
{
public List drawns { get; set; }
}

API Class
public class APIReader
{
ObservableCollection drawns;
public APIReader() {
this.drawns = new ObservableCollection();
}

    public ObservableCollection<Drawn> getDrawns()
    {
        return drawns;
    }

    public  async void getLastDrawnASync()
    {
        var client = new System.Net.Http.HttpClient();
        client.BaseAddress = new Uri("https://nunofcguerreiro.com/");
        var response = await client.GetAsync("api-euromillions-json");
        var drawnsJson = response.Content.ReadAsStringAsync().Result;
        var rootobject =  JsonConvert.DeserializeObject<RootObject>(drawnsJson);
        foreach (var drawn_json in rootobject.drawns)
        {
            this.drawns.Add(drawn_json);
        }
    }
}

On debuging I get "Drawn" objets in the rootobject.drawns list , but they are not being added to my ObservableCollection. I haven't figured out yet the problem. Im currently new at C# and Xamarin, would be nice to get some help from you guys.

Thanks in advance

Posts

  • Nadjib_BaitNadjib_Bait DZMember ✭✭✭✭

    Hey. Could you post more code, the entire class please.

    And:

    var drawnsJson = response.Content.ReadAsStringAsync().Result;

    use instead:

    var drawnsJson = await response.Content.ReadAsStringAsync();

    because when you use .Result, you loose all the benefits of async, actually you're (probably) doing the work of getting the http response body on UI thread (which is super bad!)

  • APIReader class:
    http://pastebin.com/m2KfLiTD
    Drawn class:
    http://pastebin.com/7g3Cu6Nu
    DrawnsView class:
    http://pastebin.com/3s84u1C1

    Thanks for your help

  • Nadjib_BaitNadjib_Bait DZMember ✭✭✭✭
    edited April 2015

    Hmm... there's some overcomplication of code here and some bad practices:
    1. Never (ever) use "async void", use it only when you can't avoid it (ie. events handler), because async void will fail silently if there's an exception, so you could never detect an exception. Use instead Task.

    1. You could just have one fuction in your ApiReader that returns drawns and handle caching, let's call it GetDrawnsAsync.

    2. Just call the GetDrawnsAsync function in the OnAppearing method on your page. That's all.

    So I simplified your code to this:

    public class APIReader
    {
    IEnumerable _drawns; //no need here to have an ObservableCollection, this will be used for caching.

        public  async Task<IEnumerable<Drawn>> GetLastDrawnsAsync() // Try follow C# naming conventions, methods should you "CamelCase" lettering, only local vars should be named like "localVar"
        {
            // Add "using System.Linq;" to declarations, so we can use Linq great functions like .Any(), .First(), .LastOrDefault()....etc
    
            if (this._drawns != null && this._drawns.Any()) // check if _draws is not null and has elements in it, means we already fetched the result, no need to call again the web service...
                return _drawns;                             // you can add some logic refresh caching based on last time we fetched results.
    
    
            this._drawns = new List<Drawn>();
    
            var client = new System.Net.Http.HttpClient();
            client.BaseAddress = new Uri("https://nunofcguerreiro.com/");
            var response = await client.GetAsync("api-euromillions-json");
            response.EnsureStatusCodeSuccess(); //I'm writing this in the top of my head, not sure what's the method name, we use this to be sure that our request is successful, else it'll throw an exception!
                                                //and this illustrates why we should never use async void methods, because this exception will be "swallowed" and we can't know if the request succeded.
    
            var drawnsJson = await response.Content.ReadAsStringAsync(); //when you use .Result it doesn't runs asynchronously!!! so you loose all the benefits of this C# awesomeness
            var rootobject =  JsonConvert.DeserializeObject<RootObject>(drawnsJson);
            foreach (var drawn_json in rootobject.drawns)
            {
                this._drawns.Add(drawn_json);
            }
    
            return _draws;
        }
    }
    

    public partial class DrawnsView : ContentPage
    {
    // You should try start learning MVVM patterns...

                private APIReader api;
                public DrawnsView ()
                {
                        InitializeComponent ();
                        Drawns = new ObservableCollection<Drawn> ();
                        api = new APIReader();
                        BindingContext = this;
                }
    
                public ObservableCollection<Drawn> Drawns {
                        get;
                        set;
                }
    
                protected override async void OnAppearing() //here we're forced to use async void because we're overriding a method, but the good news is that we can catch exceptions on our api call
                                                            // we needed to add async so we can use await in our methods, so whenever you want to use await, just add async to a method
                {
                        base.OnAppearing();
    
                        try 
                        {
                            var fetchedDrawns = await GetLastDrawnsAsync();
    
                            if (fetchedDrawns != null) //just to be sure...
                            { 
                                foreach(var drawn in fetchedDrawns) 
                                {
                                    Drawns.Add(drawn);
                                }
                            }
                        }
                        catch (Exception ex) 
                        {
                            //The request failed!! we should perhaps show a friendly error message to the user, and log it for eg.
    
                        }
    
    
                }
    
                private void DrawnsList_OnItemSelected(object sender, SelectedItemChangedEventArgs e)
                {
    
                        if (LastDrawns.SelectedItem == null) 
                        {
                                return;
                        }
    
                        LastDrawns.SelectedItem = null;
                }
    
        }
    
  • Nadjib_BaitNadjib_Bait DZMember ✭✭✭✭
    edited April 2015

    I wrote this on NotePad++, so not sure if it compiles straight forward, but if there's any problem it should be easy to fix.

  • Although we are defining initializing _drawns as a List it is not allowing me to use the "Add" methode because we are defining it as a IEnumerable on the class. Was it supposed to happen or am I missing something ?

    Sorry for this basic questions and really thanks for your help.

  • Nadjib_BaitNadjib_Bait DZMember ✭✭✭✭
    edited April 2015

    Ah yes sorry!

    You can make drawn a List, or in GetDrawnsAsync just create a new List, added all drawns to it, then assign it to _drawns at the end, before returning (_draws = drawnsList; return _drawns;)

    BTW did the code work?

  • The code indeed work. Really thanks for help!

  • vestvest USMember

    Hi @nadjib Im having the same problem. I follow the codes you wrote above to implement in my json data but still no success.

    Below is my sample json data, i want to get the "value" of the "key" object if string is CITY inside the "metadata" which also inside the "posts".

    {
    "posts": [
    {
    "ID": 2500,
    "site_ID": 1,
    "date": "2014-09-26T15:58:23-10:00",
    "title": "DOD HQ Visitors Parking",
    "metadata": [
    {
    "id": "15064",
    "key": "city",
    "value": "Honolulu County"
    },
    {
    "id": "15067",
    "key": "country",
    "value": "US"
    },
    {
    "id": "15062",
    "key": "floor_level",
    "value": "Ground Floor"
    }
    ]
    }
    ]

    }

    I'm currently new to parsing Json Data and Xamarin. Thank you :smile:

  • Nadjib_BaitNadjib_Bait DZMember ✭✭✭✭
    edited February 2017

    Here are your C# json converted into classes.

    You just deserialize that json string to RootObject class (you can rename the class a better name if you want) by using:

    var result = JsonConvert.DeserializeObject<RootObject>(jsonString); // you can access all properties and values using result object.
    
    public class Metadata
    {
        public string id { get; set; }
        public string key { get; set; }
        public string value { get; set; }
    }
    
    public class Post
    {
        public int ID { get; set; }
        public int site_ID { get; set; }
        public string date { get; set; }
        public string title { get; set; }
        public List<Metadata> metadata { get; set; }
    }
    
    public class RootObject
    {
        public List<Post> posts { get; set; }
    }
    

    Hint: Use this website to generate classes from a json input: http://json2csharp.com

  • vestvest USMember

    @nadjib I am trying to get those values (underlined in red) to bind with my listview.

    So far here's what I've done but nothing of success yet.

    Model Class - http://pastebin.com/HPyms0Dk

    VM Class - http://pastebin.com/sMCsbsFh

    XAMLPage Class - http://pastebin.com/6B0pcMD5

  • Nadjib_BaitNadjib_Bait DZMember ✭✭✭✭
    edited February 2017

    It will be better to flatten the data to a single DTO class to make it easier for binding.

    public class PostDTO
    {
        public string Title { get; set; }
        public string City { get; set; }
        public string Country {get; set; }
    }
    

    Then fill data like this:

            var fetchedSpaces = await GetPostsAsync();
    
                if (fetchedSpaces !=null)
                {
                    foreach(var space in fetchedSpaces)
                    {
                        Posts.Add(new PostDTO // Posts is an ObservableCollection<PostDTO>, you'll use it as the ListView ItemsSource 
                         {
                            Title =  space.title,
                            City = space.metadata[0].value,
                            Country= space.metadata[1].value,
    
                         });
                    }
                }
    
    
    
    
    <ListView ItemsSource="{Binding Posts}">
         <ListView.ItemTemplate>
              <DataTemplate>
                    <ViewCell>
                        <StackLayout>
                            <Label Text="{Binding Title}" />
                            <Label Text="{Binding City}" />
                            <Label Text="{Binding Country}" />
                        </StackLayout>
                    </ViewCell>
              </DataTemplate>
         </ListView.ItemTemplate>
    </ListView>
    

    Also you're having a VM class but using the XAML code behind file as VM... choose only one (preferably the VM file) to have loading logic and Posts collection in it. In that case you'll need to set the BindingContext of the page to the VM file and calling the VM loading method from OnAppearing of the page. So your XAML code behind page should look like this:

    public partial class SpacesXamlPage : ContentPage
        {
            private SpaceViewModel space_vm;
    
            public SpacesXamlPage()
            {
                InitializeComponent();
                space_vm = new SpaceViewModel();
                BindingContext = space_vm;
        }
    
        protected override async void OnAppearing()
        {
            base.OnAppearing();
    
            try
            {
               await space_vm.GetPostsAsync();
            }
           catch (Exception ex)
            {
    
            }
        }
    }
    
  • vestvest USMember

    Great! Thanks @nadjib , your code works.

Sign In or Register to comment.