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?
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
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.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.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
.LoadMoreItemsAsync(1)
followed by a single call to LoadMoreItemsAsync(> 1)
if not all of the items have been loaded by the prior calls.What causes the problem
LoadMoreItemsAsync
method before you've added the items to the list (awaiting tasks after you've added the items to the list is fine).await
s 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.await
s, 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With