Best way to load a grouped collection on Xamarin.Forms ListView/CollectionView with infinite scrolling

Tharindu Perera 31 Reputation points
2021-08-10T13:47:26.743+00:00

What would be the best way to load a grouped collection on Xamarin.Forms ListView/CollectionView with incremental loading?

For example, say a collection contains several groups and each group contains a list of items.

[
    [123, 143, 341234, 234234, 514232, 23511, 673456, ...],
    [12, 143, 341234, 234234, 514232, 23511, , ...],
    [12, 143, 341234, 234234, 514232, 23511, 313, ...],
    [12, 143, 341234, 514232, 23511, 673456, ...],
    [12, 143, 341234, 234234, 514232, 132, 23511, 673456, ...],
    .
    .
    .
    [12, 143, 341234, 234234, 514232, 23511, 673456, ...],
]

Update

With one dimensional list, I could load the data into the ListView or CollectionView using ListView.ItemAppearing/CollectionView.RemainingItemsThresholdReached events.

Infinite Scroll with Xamarin.Forms CollectionView

Load More Items at End of ListView in Xamarin.Forms

listView.ItemAppearing += ListView_ItemAppearing;
IList<Item> originalList = new List<Item> {Item1, ..., Item10000};

private void ListView_ItemAppearing(object sender, ItemVisibilityEventArgs e)
{
    if (// all the items in the original list loaded into the listview) 
    {
        listView.ItemAppearing -= ListView_ItemAppearing;
    }
    else
    {
        // Add next set of items from the original list to the listview
    }
}

So my concern is, what would be the best way (or the best practice) to load incrementally a grouped collection into a ListView or CollectionView?

Xamarin
Xamarin
A Microsoft open-source app platform for building Android and iOS apps with .NET and C#.
5,325 questions
0 comments No comments
{count} votes

Accepted answer
  1. JarvanZhang 23,951 Reputation points
    2021-08-11T07:42:57.197+00:00

    Hello @Tharindu Perera ,​

    Welcome to our Microsoft Q&A platform!

    to load incrementally a grouped collection into a ListView or CollectionView

    ListView

    You could still achieve the function in the listView's ItemAppearing event. When the listView is grouped, the itemIndex is from the items of all the groups and the items also contain the group line. Try to caculate the count of all the items and then detect the value of ItemIndex.

    Here is the sample code, you could refer to it.

       public partial class TestPage : ContentPage  
       {  
           public ObservableCollection<ViewGroup> DataCollection { get; set; }  
           int number = 0;  
    
           public TestPage()  
           {  
               InitializeComponent();  
    
               DataCollection = new ObservableCollection<ViewGroup>();  
               DataCollection.CollectionChanged += DataCollection_CollectionChanged;  
    
               //add items to the dataCollection  
    
               BindingContext = this;  
           }  
    
           private void DataCollection_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)  
           {  
               number = 0;  
               foreach (ViewGroup item in DataCollection)  
               {  
                   number += item.Count + 1; //caculate all the items, 1 is the each group line  
               }  
           }  
           private void listView_ItemAppearing(object sender, ItemVisibilityEventArgs e)  
           {  
               if (e.ItemIndex == number - 1)  
               {  
                   DataCollection.Add(new ViewGroup("group_" + (DataCollection.Count + 1), new ObservableCollection<TestModel>() {  
                       //items  
                   }));  
               }  
           }  
       }  
    
       <ListView ItemsSource="{Binding DataCollection}"   
                 GroupDisplayBinding="{Binding GroupTitle}"   
                 HasUnevenRows="True"   
                 ItemAppearing="listView_ItemAppearing"   
                 IsGroupingEnabled="True">  
           <ListView.ItemTemplate>  
               ...  
           </ListView.ItemTemplate>  
       </ListView>  
    

    The group model class

       public class ViewGroup : Collection<TestModel>  
       {  
           public string GroupTitle { get; set; }  
           public ViewGroup(string groupTitle, IList<TestModel> list) : base(list)  
           {  
               this.GroupTitle = groupTitle;  
           }  
       }  
    

    CollectionView

    We can just achieve the infinite scrolling in a grouped collectionView using RemainingItemsThresholdReached event as in the normal collectionView.

       <CollectionView   
           ItemsSource="{Binding DataCollection}"   
           RemainingItemsThresholdReached="CollectionView_RemainingItemsThresholdReached"   
           RemainingItemsThreshold="1"   
           IsGrouped="True">  
           ...  
       </CollectionView>  
    
       private void CollectionView_RemainingItemsThresholdReached(object sender, EventArgs e)  
       {  
           DataCollection.Add(new ViewGroup("group_" + (DataCollection.Count + 1), new ObservableCollection<Page1Model>() {  
               //items  
           }));  
       }  
    

    The code work as expected on Android, but not on iOS. Someone faced the issue and has reported it to the github, RSchipper shared a solution to fix the issue. You could refer to the code: https://github.com/xamarin/Xamarin.Forms/issues/8383#issuecomment-578150883

    Best Regards,

    Jarvan Zhang


    If the response is helpful, please click "Accept Answer" and upvote it.

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    1 person found this answer helpful.

1 additional answer

Sort by: Most helpful
  1. Tharindu Perera 31 Reputation points
    2021-09-07T07:43:54.32+00:00

    @JarvanZhang thank you for your answer. You have answered one part of my question, and I will add my findings to the other part.

    to load incrementally a grouped collection into a ListView or CollectionView?

    @JarvanZhang have answered this perfectly, and this is the way to go with a grouped collection.

    But when the collection has a small number of groups (for example 5-10), and each group consists of (like1,000 items) adding group by group won't come in handy. The first reason why we are using incremental loading is to improve the performance (especially the startup) of the ListView/CollectionView.

    Here's the other part of my question:

    what would be the best way (or the best practice)?

    There might be a better way of doing this, but here's how I implemented it:

    public class ListViewModel  
    {  
        private IList<Group> _localItems = new List<Group>();  
        private int _remainingItemsToAdd = 50; // ItemsPerPage   
        private const int ItemsPerPage = 50;  
        public ObservableCollection<Group> BindedCollection { get; } = new ObservableCollection<Group>();  
    
        public ListViewModel()  
        {  
            Init();  
        }  
    
        public void SetupNextPage()  
        {  
            if (!_localItems.Any())  
                return;  
    
            // load top group on local list  
            var listTopGroup = _localItems.First();  
            if (!listTopGroup.Any())  
            {  
                _localItems.Remove(listTopGroup);  
                SetupNextPage();  
                return;  
            }  
    
            if (!BindedCollection.Any() || BindedCollection.Last().Id != listTopGroup.Id)  
            {  
                BindedCollection.Add(new Group(listTopGroup.Id, listTopGroup.GroupTitle));  
            }  
    
            // get last group of the binded collection  
            var colLastGroup = BindedCollection.Last();  
            var itemsToAdd = listTopGroup.Take(_remainingItemsToAdd).ToArray();  
    
            if (itemsToAdd.Length < _remainingItemsToAdd)  
            {  
                _remainingItemsToAdd -= itemsToAdd.Length;  
    
                foreach (var item in itemsToAdd)  
                {  
                    colLastGroup.Add(item);  
                    listTopGroup.Remove(item);  
                }  
    
                SetupNextPage();  
                return;  
            }  
    
            foreach (var item in itemsToAdd)  
            {  
                colLastGroup.Add(item);  
                listTopGroup.Remove(item);  
            }  
            _remainingItemsToAdd = ItemsPerPage;  
    
        }  
    
        public void Init()  
        {  
            for (int i = 0; i < 5; i++)  
            {  
                var group = new Group(i, $"group-{i}");  
                for (int j = 0; j < 40; j++)  
                {  
                    group.Add($"item-{j}");  
                }  
                _localItems.Add(group);  
            }  
    
            SetupNextPage();  
        }  
    
        public class Group : ObservableCollection<string>  
        {  
            public int Id { get; set; }  
            public string GroupTitle { get; set; }  
    
            public Group(int id, string title)  
            {  
                Id = id;  
                GroupTitle = title;  
            }  
        }  
    }  
    

    This can be used in both CollectionView and ListView using @JarvanZhang 's answer.
    Ex:

    // with ListView.ItemAppearing  
    private void listView_ItemAppearing(object sender, ItemVisibilityEventArgs e)  
    {  
        if (e.ItemIndex == number - 1)  
        {  
            viewModel.SetupNextPage();  
        }  
    }  
    

    However, I faced this bug in ListView on iOS when using with RecycleElement caching strategy. When you try to add items to the ListView in the ItemAppearing event, the ListView will not show any updates.
    https://github.com/xamarin/Xamarin.Forms/issues/3619

    I was able to fix it with Basssiiie's workaround: https://github.com/xamarin/Xamarin.Forms/issues/3619#issuecomment-626330965

    private void listView_ItemAppearing(object sender, ItemVisibilityEventArgs e)  
    {  
        if (e.ItemIndex == number - 1)  
        {  
            Device.BeginInvokeOnMainThread(viewModel.SetupNextPage);  
        }  
    }  
    
    1 person found this answer helpful.
    0 comments No comments