Xamarin forms: Listview grouping issue

SreeeeSreeee INMember ✭✭✭✭✭

I am trying to implement listview grouping for my following JSON data.

JSON Sample:

{ 
   "cbrainBibleBooksHB":[ { 
        "book":"2 John",
         "cbrainBibleTOList":[ 
            { 
               "bookName":"2 John",
               "chapter":"1",
               "pageUrl":"/edu-bible/9005/1/2-john-1"
            },
            {....}
         ]
      },
      { 
         "book":"3 John",
         "cbrainBibleTOList":[ 
            {  
               "bookName":"3 John",
               "chapter":"1",
               "pageUrl":"/edu-bible/9007/1/3-john-1"
            },
            {...}
         ]
      }
   ]
 }

I am trying to group the JSON data by its book name.

I tried like below:

Model:

public class BibleTestament
    {
        public List<CbrainBibleBooksHB> cbrainBibleBooksHB { get; set; }
    }

    public class CbrainBibleBooksHB : ObservableCollection<CbrainBibleTOList>
    {
        public string book { get; set; }
        public List<CbrainBibleTOList> cbrainBibleTOList { get; set; }
    }

    public class CbrainBibleTOList
    {
        public string chapter { get; set; }
        public string pageUrl { get; set; }
        public string bookName { get; set; }
    }

Viewmodel

HttpClient client = new HttpClient();
                var Response = await client.GetAsync("rest api");
                if (Response.IsSuccessStatusCode)
                {
                    string response = await Response.Content.ReadAsStringAsync();
                    Debug.WriteLine("response:>>" + response);
                    BibleTestament bibleTestament = new BibleTestament();
                    if (response != "")
                    {
                        bibleTestament = JsonConvert.DeserializeObject<BibleTestament>(response.ToString());
                    }
                    AllItems = new ObservableCollection<CbrainBibleBooksHB>(bibleTestament.cbrainBibleBooksHB);

XAML

<ContentPage.Content>
        <StackLayout>
            <ListView 
                HasUnevenRows="True"
                ItemsSource="{Binding AllItems,Mode=TwoWay}"
                IsGroupingEnabled="True">
                <ListView.GroupHeaderTemplate>
                    <DataTemplate>
                        <ViewCell>
                            <Label 
                                Text="{Binding book}"
                                Font="Bold,20" 
                                HorizontalOptions="CenterAndExpand"
                                HorizontalTextAlignment="Center"
                                VerticalTextAlignment="Center"
                                Margin="3"
                                TextColor="Black"
                                VerticalOptions="Center"/>
                        </ViewCell>
                    </DataTemplate>
                </ListView.GroupHeaderTemplate>
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell>
                            <ViewCell.View>
                                <StackLayout
                                    HorizontalOptions="StartAndExpand"
                                    VerticalOptions="FillAndExpand"
                                    Orientation="Horizontal">

                                    <Label 
                                        Text="{Binding cbrainBibleTOList.chapter}"
                                        Font="20" 
                                        HorizontalTextAlignment="Center"
                                        VerticalTextAlignment="Center"
                                        HorizontalOptions="CenterAndExpand"
                                        TextColor="Black"
                                        VerticalOptions="Center"/>
                                </StackLayout>
                            </ViewCell.View>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
                <ListView.Footer>
                    <Label/>
                </ListView.Footer>
            </ListView>
        </StackLayout>
    </ContentPage.Content>

But no data is showing on the UI when running the project. Getting Binding: 'book' property not found on 'System.Object[]', target property: 'Xamarin.Forms.Label.Text' message on output box. It is very difficult to implement grouping for a listview in xamarin forms. :( Can anyone help me to do this? I have uploaded a sample project here.

Best Answers

  • LeonLuLeonLu Xamurai
    Accepted Answer

    @Sreeee I test your demo. Here are some mistakes in your code. I change serveral places, here is running GIF.

    First of all, your JSON data is too large and android should connect to your url, this time is over 5 seconds that makes listview cannot load these data at once. If I put your JSON data in the local file, Then I read the Json file from this local file, we could see the result correctly.

    Then, your JSON deserialization class and the data struct of the Model are wrong. If you want to get the correct JSON deserialization class, you can use this website, put your json data in it, then it will generate this class.
    http://json2csharp.com/

    I changed the class of JSON deserialization and data struct of the Model like following format.

    class of JSON deserialization RootObject.cs

          public class RootObject
    {
        public ObservableCollection<CbrainBibleBooksHB> cbrainBibleBooksHB { get; set; }
    }
    public class CbrainBibleBooksHB
    {
        public string book { get; set; }
        public ObservableCollection<CbrainBibleTOList> cbrainBibleTOList { get; set; }
    }
    public class CbrainBibleTOList
    {
        public int bibleId { get; set; }
        public int orderNo { get; set; }
        public string testament { get; set; }
        public string bookName { get; set; }
        public string chapter { get; set; }
        public int applicationId { get; set; }
        public int siteId { get; set; }
        public int userCreated { get; set; }
        public int userModified { get; set; }
        public object createdTime { get; set; }
        public object updatedTime { get; set; }
        public string pageUrl { get; set; }
    }
    

    data struct of the Model PageTypeGroup.cs

           public class PageTypeGroup: ObservableCollection<MyCbrainBibleTOList>
    {
        public string Book { get; set; }
    
    }
    
    public class MyCbrainBibleTOList
    {
        public string chapter { get; set; }
        public string bookName { get; set; }
        public string pageUrl { get; set; }
        public MyCbrainBibleTOList(string chapter, string bookName, string pageUrl)
        {
            this.chapter = chapter;
            this.bookName = bookName;
            this.pageUrl = pageUrl;
        }
    
    }
    

    You could refer to this link, it will told you about group of listview.
    https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/customizing-list-appearance

    I upload my demo to you, you can refer to it.

  • SreeeeSreeee IN ✭✭✭✭✭
    Accepted Answer

    Copied answer from my Stack overflow thread.

    You can use the latest BindableLayout of Xamarin.Forms version >=3.5 instead of using grouped Listview with less effort involved.

    Update your Model class

    public class CbrainBibleBooksHB
    {
       public string book { get; set; }
       public List<CbrainBibleTOList> cbrainBibleTOList { get; set; }
    }
    

    XAML:

    <ScrollView>
        <FlexLayout
            BindableLayout.ItemsSource="{Binding AllItems}"
            Direction="Column"
            AlignContent="Start">
            <BindableLayout.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="30"/>
                            <RowDefinition Height="auto"/>
                        </Grid.RowDefinitions>
                        <Label Grid.Row="0"
                                Text="{Binding book}"
                                HorizontalOptions="FillAndExpand"
                                BackgroundColor="LightBlue"/>
                        <StackLayout Grid.Row="1"
                                        BindableLayout.ItemsSource="{Binding cbrainBibleTOList}">
                            <BindableLayout.ItemTemplate>
                                <DataTemplate>
                                    <Label Text="{Binding chapter}">
                                        <Label.GestureRecognizers>
                                            <TapGestureRecognizer Command="{Binding BindingContext.TapCommand, Source={x:Reference Name=ParentContentPage}}" CommandParameter="{Binding .}"/>
                                        </Label.GestureRecognizers>
                                    </Label>
                                </DataTemplate>
                            </BindableLayout.ItemTemplate>
                        </StackLayout>
                    </Grid>
                </DataTemplate>
            </BindableLayout.ItemTemplate>
        </FlexLayout>
    </ScrollView>
    

    Note: Here ParentContentPage is the x:Name of parent content page which is used to give reference for command.

    ViewModel:

    class BibleTestamentViewModel : INotifyPropertyChanged
    {
        public ICommand TapCommand { get; private set; }
    
        public BibleTestamentViewModel()
        {
            TapCommand = new Command(ChapterClickedClicked);
        }
    
        private void ChapterClickedClicked(object sender)
        {
            //check value inside sender
        }
    }
    

    Output:

    enter image description here

Answers

  • LeonLuLeonLu Member, Xamarin Team Xamurai
    edited October 10

    @Sreeee
    You can refer to Lucas's reply. He have given your answer.
    https://stackoverflow.com/questions/58319878/xamarin-formslistview-grouping-issue

  • JohnHJohnH GBMember ✭✭✭✭✭

    Your code is a little bit all over the place, you have 3 lists but only need 1.
    CbrainBibleBooksHB is already a list, it doesn't need to contain another list, so remove that.
    Then you wrapped bibleTestament.cbrainBibleBooksHB in another list, don't do that either. bibleTestament.cbrainBibleBooksHB IS your list.
    Is AllItems on the view model used by the page? Assign bibleTestament.cbrainBibleBooksHB to it if so.

    Try that and see how you get on. Reading the link from @igorkr_10 above is also a very good idea.

  • SreeeeSreeee INMember ✭✭✭✭✭
    edited October 10

    @LeonLu I am working on Lucas's reply, I will give you an update soon.
    I have doubts about how to add data to the collection after the REST API call. Checking that now.

  • SreeeeSreeee INMember ✭✭✭✭✭
    edited October 10

    Getting an exception after make the changes suggested by Lucas.

    Exception111:::>Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'MyProject.Model.CbrainBibleBooksHB' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
    To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
    Path 'cbrainBibleBooksHB', line 1, position 22.

    My code:

       HttpClient client = new HttpClient();
                    var Response = await client.GetAsync("REST API Url");
                    if (Response.IsSuccessStatusCode)
                    {
                        string response = await Response.Content.ReadAsStringAsync();
                        CbrainBibleBooksHB cbrainBibleBooksHB = new CbrainBibleBooksHB();
                        if (response != "")
                        {
                //Exception is on the below line
                            cbrainBibleBooksHB = JsonConvert.DeserializeObject<CbrainBibleBooksHB>(response.ToString());
                        }
        }
    

    My model

    public class CbrainBibleBooksHB : ObservableCollection<CbrainBibleTOList>
        {
            public string book { get; set; }
            ///public List<CbrainBibleTOList> cbrainBibleTOList { get; set; }
        }
    
        public class CbrainBibleTOList
        {
            public string chapter { get; set; }
            public string pageUrl { get; set; }
        }
    

    And how can I add books wise data to the collection after the REST API call?

  • igorkr_10igorkr_10 Member ✭✭✭
    edited October 10

    @Sreeee Show your JSON example

  • JohnHJohnH GBMember ✭✭✭✭✭

    You shouldn't be deserializing your JSON directly into your view model, you deserialize into DTO objects that match your JSON, then map the DTO object properties onto/into your view model.

    You have 2 separate issues to solve, your JSON deserialization and then your view model. Don't confuse them.

  • LeonLuLeonLu Member, Xamarin Team Xamurai
    Accepted Answer

    @Sreeee I test your demo. Here are some mistakes in your code. I change serveral places, here is running GIF.

    First of all, your JSON data is too large and android should connect to your url, this time is over 5 seconds that makes listview cannot load these data at once. If I put your JSON data in the local file, Then I read the Json file from this local file, we could see the result correctly.

    Then, your JSON deserialization class and the data struct of the Model are wrong. If you want to get the correct JSON deserialization class, you can use this website, put your json data in it, then it will generate this class.
    http://json2csharp.com/

    I changed the class of JSON deserialization and data struct of the Model like following format.

    class of JSON deserialization RootObject.cs

          public class RootObject
    {
        public ObservableCollection<CbrainBibleBooksHB> cbrainBibleBooksHB { get; set; }
    }
    public class CbrainBibleBooksHB
    {
        public string book { get; set; }
        public ObservableCollection<CbrainBibleTOList> cbrainBibleTOList { get; set; }
    }
    public class CbrainBibleTOList
    {
        public int bibleId { get; set; }
        public int orderNo { get; set; }
        public string testament { get; set; }
        public string bookName { get; set; }
        public string chapter { get; set; }
        public int applicationId { get; set; }
        public int siteId { get; set; }
        public int userCreated { get; set; }
        public int userModified { get; set; }
        public object createdTime { get; set; }
        public object updatedTime { get; set; }
        public string pageUrl { get; set; }
    }
    

    data struct of the Model PageTypeGroup.cs

           public class PageTypeGroup: ObservableCollection<MyCbrainBibleTOList>
    {
        public string Book { get; set; }
    
    }
    
    public class MyCbrainBibleTOList
    {
        public string chapter { get; set; }
        public string bookName { get; set; }
        public string pageUrl { get; set; }
        public MyCbrainBibleTOList(string chapter, string bookName, string pageUrl)
        {
            this.chapter = chapter;
            this.bookName = bookName;
            this.pageUrl = pageUrl;
        }
    
    }
    

    You could refer to this link, it will told you about group of listview.
    https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/customizing-list-appearance

    I upload my demo to you, you can refer to it.

  • SreeeeSreeee INMember ✭✭✭✭✭
    Accepted Answer

    Copied answer from my Stack overflow thread.

    You can use the latest BindableLayout of Xamarin.Forms version >=3.5 instead of using grouped Listview with less effort involved.

    Update your Model class

    public class CbrainBibleBooksHB
    {
       public string book { get; set; }
       public List<CbrainBibleTOList> cbrainBibleTOList { get; set; }
    }
    

    XAML:

    <ScrollView>
        <FlexLayout
            BindableLayout.ItemsSource="{Binding AllItems}"
            Direction="Column"
            AlignContent="Start">
            <BindableLayout.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="30"/>
                            <RowDefinition Height="auto"/>
                        </Grid.RowDefinitions>
                        <Label Grid.Row="0"
                                Text="{Binding book}"
                                HorizontalOptions="FillAndExpand"
                                BackgroundColor="LightBlue"/>
                        <StackLayout Grid.Row="1"
                                        BindableLayout.ItemsSource="{Binding cbrainBibleTOList}">
                            <BindableLayout.ItemTemplate>
                                <DataTemplate>
                                    <Label Text="{Binding chapter}">
                                        <Label.GestureRecognizers>
                                            <TapGestureRecognizer Command="{Binding BindingContext.TapCommand, Source={x:Reference Name=ParentContentPage}}" CommandParameter="{Binding .}"/>
                                        </Label.GestureRecognizers>
                                    </Label>
                                </DataTemplate>
                            </BindableLayout.ItemTemplate>
                        </StackLayout>
                    </Grid>
                </DataTemplate>
            </BindableLayout.ItemTemplate>
        </FlexLayout>
    </ScrollView>
    

    Note: Here ParentContentPage is the x:Name of parent content page which is used to give reference for command.

    ViewModel:

    class BibleTestamentViewModel : INotifyPropertyChanged
    {
        public ICommand TapCommand { get; private set; }
    
        public BibleTestamentViewModel()
        {
            TapCommand = new Command(ChapterClickedClicked);
        }
    
        private void ChapterClickedClicked(object sender)
        {
            //check value inside sender
        }
    }
    

    Output:

    enter image description here

  • SreeeeSreeee INMember ✭✭✭✭✭

    @LeonLu Thank you very much :)

Sign In or Register to comment.