locked
ISupportIncrementalLoading ListView loads too many items only when performing an async operation in LoadMoreItemsAsync()

    Question

  • This is such an unusual bug, and I can't understand why it is happening without knowing the internal ListView code responsible for loading more items for an ISupportIncrementalLoading ItemsSource.

    I've created a very basic incremental loading ListView example in C#. It's a universal windows runtime project; the bug persists between WP and Windows. This is the code for the incremental loading collection:

    class IncrementalLoadingCollection : ObservableCollection<string>, ISupportIncrementalLoading
    {
        int offset = 0;
    
        public bool HasMoreItems { get { return offset < 1000; } }
    
        public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
        {
            var dispatcher = Window.Current.Dispatcher;
    
            return AsyncInfo.Run(async cancelToken =>
            {
                System.Diagnostics.Debug.WriteLine("Loading {0} items", count);
    
                await dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                {
                    for (var i = offset; i < offset + count; i++)
                        Add(i.ToString());
                });
                        
                offset += (int)count;
                return new LoadMoreItemsResult { Count = count };
            });
        }
    }

    Now, the normal flow of events is this:

    • LoadMoreItemsAsync(1)  // the first time it gets called, it asks for only one item
    • LoadMoreItemsAsync(54)  // the next time it loads the correct amount (may not be 54, it depends on the size of each item and the size of the ListView)

    I'm not sure why it requests for only one item the first time, my guess is that the ListView needs to know the height of one item first in order to know how many items it should request the next time.

    In order to observe the normal flow of events, the "async cancelToken => ..." lambda mustn't contain any await operations. That is, it should be a synchronous method. So remove the "await dispatcher.RunAsync(...)" wrapper and it will work correctly.

    If we use the code as it is above with the await operation, then what actually happens is the ListView will call LoadMoreItemsAsync(1) many times, and LoadMoreItemsAsync(54) may not even be called depending on whether or not too many items have been loaded for HasMoreItems to become false. This means anywhere between 55 - 1000 items will be loaded once the page has loaded.

    Interestingly, even this code is problematic:

    class IncrementalLoadingCollection : ObservableCollection<string>, ISupportIncrementalLoading
    {
        int offset = 0;
    
        public bool HasMoreItems { get { return offset < 1000; } }
    
        public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
        {
            return AsyncInfo.Run(async cancelToken =>
            {
                System.Diagnostics.Debug.WriteLine("Loading {0} items", count);
    
                await Task.Run(() => { });
    
                for (var i = offset; i < offset + count; i++)
                    Add(i.ToString());
                        
                offset += (int)count;
                return new LoadMoreItemsResult { Count = count };
            });
        }
    }

    By including just "await Task.Run(() => { });" into the lambda will cause it to load too many items!

    If I use instead something like "await Task.Delay(1000);" then the issue doesn't occur. If I use "await Task.Delay(1);" then LoadMoreItemsAsync(1) will be called maybe one or two more times than necessary only.

    If I put the await after I have added the items to the collection, then the issue doesn't occur.

    Why is this happening and what is causing it?

    You can download my code from github to see exactly what I'm describing.


    • Edited by DecadeMoon Wednesday, November 5, 2014 2:41 AM Fix error
    Wednesday, November 5, 2014 2:40 AM

All replies

  • Hi DecadeMoon,

    I can reproduce some of your scenarios. But I cannot tell you what happened because the LoadMoreItemsAsync is called by system at runtime. The system will decide to execute this method. So what we can do is to work around this issue. Try the MSDN code example. https://code.msdn.microsoft.com/windowsapps/Data-Binding-7b1d67b5.

    Regards,


    We are trying to better understand customer views on social support experience, so your participation in this interview project would be greatly appreciated if you have time. Thanks for helping make community forums a great place. Click HERE to participate the survey.

    Thursday, November 6, 2014 5:07 AM
    Moderator
  • Hi Herro wong,

    Thanks for responding. What does this mean then? Is it a bug?

    Sure, I can work around it. I can put a

    await Task.Delay(10);
    before the dispatcher.RunAsync() task, but it seems kinda hacky. My issue is understanding why this behavior is happening, and why awaiting a short lived task, such as dispatcher.RunAsync, or even Task.Run(() => {}), causes the issue.
    Saturday, November 8, 2014 1:02 PM