Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a ListView with LoadMoreItemsAsync on end of scroll

I have a ListView in my Windows Phone 8.1 application and I can have something like 1000 or more results, so I need to implement a Load More feature each time the scroll hits bottom, or some other logic and natural way of triggering the adding of more items to the List.
I found that the ListView has support for an ISupportIncrementalLoading, and found this implementation: https://marcominerva.wordpress.com/2013/05/22/implementing-the-isupportincrementalloading-interface-in-a-window-store-app/ This was the better solution I found, since it does not specify a type, i.e., it's generic.

My problem with this solution is that when the ListView is Loaded, the LoadMoreItemsAsync runs all the times needed until it got all the results, meaning that the Load More is not triggered by the user. I'm not sure what make the LoadMoreItemsAsync trigger, but something is not right, because it assumes that happens when I open the page and loads all items on the spot, without me doing anything, or any scrolling. Here's the implementation:
IncrementalLoadingCollection.cs

public interface IIncrementalSource<T> {
    Task<IEnumerable<T>> GetPagedItems(int pageIndex, int pageSize);
    void SetType(int type);
}

public class IncrementalLoadingCollection<T, I> : ObservableCollection<I>, ISupportIncrementalLoading where T : IIncrementalSource<I>, new() {
    private T source;
    private int itemsPerPage;
    private bool hasMoreItems;
    private int currentPage;

    public IncrementalLoadingCollection(int type, int itemsPerPage = 10) {
        this.source = new T();
        this.source.SetType(type);
        this.itemsPerPage = itemsPerPage;
        this.hasMoreItems = true;
    }

    public bool HasMoreItems {
        get { return hasMoreItems; }
    }

    public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count) {
        var dispatcher = Window.Current.Dispatcher;

        return Task.Run<LoadMoreItemsResult>(
            async () => {
                uint resultCount = 0;
                var result = await source.GetPagedItems(currentPage++, itemsPerPage);

                if(result == null || result.Count() == 0) {
                    hasMoreItems = false;
                }
                else {
                    resultCount = (uint)result.Count();

                    await dispatcher.RunAsync(
                        CoreDispatcherPriority.Normal,
                        () => {
                            foreach(I item in result)
                                this.Add(item);
                        });
                }

                return new LoadMoreItemsResult() { Count = resultCount };

            }).AsAsyncOperation<LoadMoreItemsResult>();
    }
}

Here's the PersonModelSource.cs

public class DatabaseNotificationModelSource : IIncrementalSource<DatabaseNotificationModel> {
    private ObservableCollection<DatabaseNotificationModel> notifications;
    private int _type = "";

    public DatabaseNotificationModelSource() {
        //
    }

    public void SetType(int type) {
        _type = type;
    }

    public async Task<IEnumerable<DatabaseNotificationModel>> GetPagedItems(int pageIndex, int pageSize) {
        if(notifications == null) {
            notifications = new ObservableCollection<DatabaseNotificationModel>();
            notifications = await DatabaseService.GetNotifications(_type);
        }

        return await Task.Run<IEnumerable<DatabaseNotificationModel>>(() => {
            var result = (from p in notifications select p).Skip(pageIndex * pageSize).Take(pageSize);
                return result;
            });
    }
}

I changed it a bit, because the call to my Database is Asynchronous and it was the only way I found to make sure I could wait for the query before filling the collection.

And in my DatabaseNotificationViewModel.cs

IncrementalNotificationsList = new IncrementalLoadingCollection<DatabaseNotificationModelSource, DatabaseNotificationModel>(type);


Everything works fine, apart from the not so normal "Load More". What's wrong in my code?

like image 452
Schrödinger's Box Avatar asked Nov 03 '14 17:11

Schrödinger's Box


2 Answers

I created a very simplified example of this issue here, and raised this issue on the MSDN forums here. Honestly, I don't know why this weird behavior is happening.

What I observed

  • The ListView will call LoadMoreItemsAsync first with a count of 1. I assume this is to determine the size of a single item so that it can work out the number of items to request for the next call.
  • If the ListView is behaving nicely, the second call to LoadMoreItemsAsync should happen immediately after the first call, but with the correct number of items (count > 1), and then no more calls to LoadMoreItemsAsync will occur unless you scroll down. In your example, however, it may incorrectly call LoadMoreItemsAsync with a count of 1 again.
  • In the worst case, which actually occurs quite frequently in your example, is that the ListView will continue to call LoadMoreItemsAsync with a count of 1 over and over, in order, until HasMoreItems becomes false, in which case it has loaded all of the items one at a time. When this happens, there is a noticeable UI delay while the ListView loads the items. The UI thread isn't blocked, though. The ListView is just hogging the UI thread with sequential calls to LoadMoreItemsAsync.
  • The ListView won't always exhaust all of the items though. Sometimes it will load 100, or 200, or 500 items. In each case, the pattern is: many calls of LoadMoreItemsAsync(1) followed by a single call to LoadMoreItemsAsync(> 1) if not all of the items have been loaded by the prior calls.
  • It only seems to occur on page load.
  • The issue is persistent on Windows Phone 8.1 as well as Windows 8.1.

What causes the problem

  • The issue seems to be very short lived awaited tasks in the LoadMoreItemsAsync method before you've added the items to the list (awaiting tasks after you've added the items to the list is fine).
  • The issue doesn't occur if you remove all awaits inside LoadMoreItemsAsync, thus forcing it to execute synchronously. Specifically, if you remove the dispatcher.RunAsync wrapper and await source.GetPagedItems (just mock the items instead), then the ListView will behave nicely.
  • Having removed all awaits, the issue will reappear even if all you add is a seemingly harmless await Task.Run(() => {}). How bizarre!

How to fix the problem

If most of the time spent in a LoadMoreItemsAsync call is waiting for a HTTP request for the next page of items, as I expect most apps are, then the issue won't occur. So, we can extend the time spent in the method by awaiting a Task.Delay(10), like this maybe:

await Task.WhenAll(Task.Delay(10), dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
    foreach (I item in result)
        this.Add(item);
}).AsTask());

I've merely provided a (hacky) workaround for your example, but not an explanation why. If anyone knows why this is happening, please let me know.

like image 195
Decade Moon Avatar answered Oct 12 '22 10:10

Decade Moon


This is not the only thing that can cause this issue. If your ListView is inside a ScrollViewer, it will continue loading all of the items and ALSO will not virtualize properly, negatively impacting performance. The solution is to give your ListView a specific height.

like image 41
Daniel Gary Avatar answered Oct 12 '22 12:10

Daniel Gary