XAML Playground
about XAML and other Amenities

Metro: Incrementally load GridView and ListView with ISupportIncrementalLoading

2012-06-10T21:58:36+01:00 by codeblock

Developers that usually deal with web applications know that one of the pillar of this kind of applications is the use of paged result sets because moving a huge number of records form the server to the browser is not a good idea. Metro applications suffer the same problem. No matter that metro applications are not strictly web applications, the application architecture imply that the connection to a datasource have to be wrapped by web service call so the need of limiting the usage of the network is a strong requirements.

Metro introduces a new interesting method to mange pagine of data. Since the use of a common paging is deprecated by guidelines the requirement is to automatically load records when a user is about to the end of the items available on the user interface. This may be an hard task to do with components like GridView and ListView but thanks to the ISupportIncrementalLoading interface it may be easy like a game.

The ISupportIncrementalLoading interface has to be implemented by a collection. When a GridView detects this interface in the class provided in the ItemsSource property, it automatically change its behavior and works loading items only when they really need to fill empty space because the user reached the end. The definition of the interface is pretty simple:

   1: public interface ISupportIncrementalLoading
   2: {
   3:     bool HasMoreItems { get; }
   4:     IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count);
   5: }

The HasMoreItems property simply inform the consumer when there is more items to load. But the core of the interface is the LoadMoreItemsAsync method. As the name suggests this method works asynchronously and it have to load a number of items. The count parameter represents the number of items the consumer needs but the body of the method can load a different number, based on its paging size, and have to return the this number as a result into the LoadMoreItemsResult class.

You may expect that the loaded items have to be returned by this method but it is not true. Remembering that the interface have to be implemented by a collection, the method have simply to append the loaded items to the collection itself. So, this interface needs that the collection raises a CollectionChange event to update the user interface. An exaample is for sure much more clear:

   1: public class NaturalNumbers : ObservableCollection<int>, ISupportIncrementalLoading
   2: {
   3:     public bool HasMoreItems
   4:     {
   5:         get { return true; }
   6:     }
   7:  
   8:     public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
   9:     {
  10:         CoreDispatcher dispatcher = Window.Current.Dispatcher;
  11:  
  12:         return Task.Run<LoadMoreItemsResult>(
  13:             () =>
  14:             {
  15:                 int[] numbers = Enumerable.Range(this.LastOrDefault(), 100);
  16:                 
  17:                 dispatcher.RunAsync(
  18:                     CoreDispatcherPriority.Normal,
  19:                     () =>
  20:                     {
  21:                         foreach (int item in numbers)
  22:                             this.Add(item);
  23:                     });
  24:     
  25:                 return new LoadMoreItemsResult() { Count = 100 };
  26:     
  27:             }).AsAsyncOperation<LoadMoreItemsResult>();
  28:     } 
  29: }

If you attach an instance of this class to a GridView (or a ListView) it will show a series of natural numbers loading them incrementally when you scroll the control. Every time the GridView reach the edge of the screen it needs to load a number of items so, after having checked the HasMoreItems propertyit call the LoadMoreItemsAsync method and await for the end of the operation. Inside this method I start a new Task. This is required because I have to return something to wait to the caller and the AsAsyncOperation converts the Task to the requested async operation. Inside the thread I generate 100 numbers starting from last in the collection then I marshal this numbers to the ui thread and load them to the collection. Since the collection is Observable this updates the items in the user interface. Finally i return the number of items I generated.

As a more complex exercise I've prepared an example attached to the end of this post. This example use the incremental strategy to load images from Flickr search API. The application shown in the following screenshot implements the search contract. When a query is made it load a special collection and the items are loaded incrementally when the user scrolls the GridView.

screenshot_06102012_224133

For this purpose I've created a IncrementalSource class. This class implements the ISupportIncrementalLoading interface and is able to manage every data source that exposes a GetPage method. If you have this method already implemented in an application you can easily turn it to incremental loading in a breeze.

   1: public class IncrementalSource<T, K> : ObservableCollection<K>, ISupportIncrementalLoading
   2:     where T: IPagedSource<K>, new()
   3: {
   4:     private string Query { get; set; }
   5:     private int VirtualCount { get; set; }
   6:     private int CurrentPage { get; set; }
   7:     private IPagedSource<K> Source { get; set; }
   8:  
   9:     public IncrementalSource(string query)
  10:     {
  11:         this.Source = new T();
  12:         this.VirtualCount = int.MaxValue;
  13:         this.CurrentPage = 0;
  14:         this.Query = query;
  15:     }
  16:  
  17:     #region ISupportIncrementalLoading
  18:     
  19:     public bool HasMoreItems
  20:     {
  21:         get { return this.VirtualCount > this.CurrentPage * 25; }
  22:     }
  23:  
  24:     public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
  25:     {
  26:         CoreDispatcher dispatcher = Window.Current.Dispatcher;
  27:  
  28:         return Task.Run<LoadMoreItemsResult>(
  29:             async () =>
  30:             {
  31:                 IPagedResponse<K> result = await this.Source.GetPage(this.Query, ++this.CurrentPage, 25);
  32:                 
  33:                 this.VirtualCount = result.VirtualCount;
  34:  
  35:                 await dispatcher.RunAsync(
  36:                     CoreDispatcherPriority.Normal,
  37:                     () =>
  38:                     {
  39:                         foreach (K item in result.Items)
  40:                             this.Add(item);
  41:                     });
  42:  
  43:                 return new LoadMoreItemsResult() { Count = (uint)result.Items.Count() };
  44:  
  45:             }).AsAsyncOperation<LoadMoreItemsResult>();
  46:     } 
  47:  
  48:     #endregion
  49: }

In my example I've implemented a Flickr class that use the flickr.photos.search API. So when a search comes I create an instance of this collection in the ItemsSource property of the GridView.

this.gv.ItemsSource = new IncrementalSource<Flickr, FlickrPhoto>(search);

If you want to try this beautiful application please download the full code from the link below. Provide your own Flicks Api Key and run the example in Visual Studio 2012 RC.

Download: http://www.silverlightplayground.org/assets/sources/XPG.Examples.IncrementalLoading.zip

Categories:  
Actions:   E-mail | del.icio.us | Permalink | Comments (11) | Comment RSSRSS comment feed

Comments (11) -

September 28. 2012 14:40

Excellent article. Have you looked into solving this issue for grouped data?

Kasper Holdum

October 7. 2012 17:45

It asks for a tfs connection when project is opening and it gives an error on project starting:
mscorlib.dll!System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()

Thank you

Ateş DANIŞ

October 8. 2012 11:10

You can simply remove the TFS binding and the project will load normally.

Andrea

October 19. 2012 01:54

This is a beautiful piece of code. I love when code is so clear that you need no comments.

On the other hand I have some questions regarding your implementation which are:

Since the collections view will never change the Query, Why the VirtualCount and  CurrentPage are members of the IncrementalSource class instead of being members of the IPagedSource?
Is it this to decouple the items provider (Source) from the current state of the object requesting the items (IncrementalSource)?
What I want to make sure in the last question is that you have the CurrentPage and VirtualCount properties in the IncrementalSource because it is each IncrementalSource task to know where they are (how many items they already have and what's the limit) instead of the Source to keep track of that, right? Otherwise those properties would be in the IPageSource.

And also, I would add the PageSize property to the IncrementalSource and request it optionally in the constructor.

Please check out the app I'm working on (Single Stream http://jonathanmv.tumblr.com/post/33830153553/single-stream-screenshotsv1).
I think it is the perfect scenario to apply this, but I need to make some adjustments so that I can include items published after the query was executed. Any suggestions would be appreciated Smile

Thank you very much for sharing this.

Jonathan Morales Vélez

October 22. 2012 12:39

Excelent, but how do you control if there are no items in the collection to show the "No results message"?

The CollectionChange event is not fired if the first search returns 0 results.

Finlord

October 24. 2012 15:32

Thanks!! it worked perfectly.

Cesar

Cesar

November 5. 2012 17:46

Nice One!
How i could Share a photo(in metro app) to facebook without using any SDK
need c# code please give a post...

Santana George

January 16. 2013 10:48

Tutto molto bello, ma...
Nel mio progetto uso una libreria PCL per il ViewModel, dato che la UI sarà sia WinRT e WP. Adesso essendo l'interfaccia ISupportIncrementalLoading specifica per WinRT, non è ovviamente utilizzabile in una PCL. Esiste un modo "Custom" per realizzare la stessa funzionalità senza l'uso di quell'interfaccia?
Premetto che il mio VM prevede già dei metodi per la ricerca, e quindi caricamento, incrementale. Sto valutando l'ipotesi di utilizzare un converter per wrappare la collezione degli elementi ad una classe come quella che proponi tu.

Fabio Piras

January 29. 2013 16:09

Nice article - it helped me a lot.
I could not get the code
... Window.Current.Dispatcher;
to work. However
var dispatcher = CoreWindow.GetForCurrentThread().Dispatcher;
works nicely.

Mats Halfvares

February 10. 2013 15:27

Excellent article... also converted the FlikrResponse class to a generic one so that is is more re-usable

    [DebuggerDisplay("PageIndex = {PageIndex} - PageSize = {PageSize} - VirtualCount = {VirtualCount}")]
    public class PagedDataResponse<T> : IPagedResponse<T>
    {
        public int VirtualCount { get; private set; }
        public IEnumerable<T> Items { get; private set; }

        public PagedDataResponse(IEnumerable<T> items, int virtualCount)
        {
            this.Items = items;
            this.VirtualCount = virtualCount;
        }
    }

Stephen

February 27. 2013 09:39

Hi I have seen the example, it's nicely coded. However I came across an issue. I tried to use "search" feature within the page rather than search charm, at that time the gridview is not getting update, but it become with 0 items. In short I can't RE-BIND the gridview, why it is happening so ?

Farhan Ghumra

Pingbacks and trackbacks (2)+