question

TharinduPerera-4994 avatar image
0 Votes"
TharinduPerera-4994 asked TharinduPerera-4994 answered

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

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?


dotnet-xamarin
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

JarvanZhang-MSFT avatar image
1 Vote"
JarvanZhang-MSFT answered JarvanZhang-MSFT edited

Hello @TharinduPerera-4994 ,​

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.


· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Hi @JarvanZhang-MSFT! sorry for the delay. Thank you for your answer, it really helped. However, on my way of implementing the incremental scrolling with a grouped collection, I got stuck with a couple of bugs in Xamarin.Forms.
1. RSchipper's workaround for #8383 worked on my sample project, but not on my real project. So I went with the ListView.
2. When using the ListView with RetainElement caching strategy the app crashed with "object not set to an instance of an object". (list item is a swipe view with some heavy bindings: this was the reason we wanted to implement incremental scrolling to improve the startup of the ListView) So I went with RecycleElement.
3. When using RecycleElement, I faced this issue on iOS. And Basssiiie's workaround solved the problem.


1 Vote 1 ·

Thanks for sharing the info.

0 Votes 0 ·
TharinduPerera-4994 avatar image
1 Vote"
TharinduPerera-4994 answered

@JarvanZhang-MSFT 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-MSFT 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-MSFT'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);
     }
 }




5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.