Hibernating Rhinos

Zero friction databases

Data Virtualization, Lazy Loading, Stealth Paging–whatever you want to call it: Here’s how to do it in Silverlight

This is a guest post by Samuel Jack, a freelancer who has worked with us on the new RavenDb Studio features.

As I have previously mentioned, the management UI for RavenDb (fondly known as the Studio) has a hot new feature. A revolutionary interface for viewing huge sets of documents which uses (gasp!) scrollbars instead of paging controls. To make this feature kind on the server (we’re averse to killing Ravens of any kind), the UI requests documents just in time, as the users scrolls each part of the list into view.

In the next few blog posts, I want to share with you how we made that work in Silverlight. I’ve published a sample on GitHub, including code under a license that enables you to use it in your own projects.

Today, to whet your appetites, I’ll show you how easy it is to create user interfaces that display huge collections of items. In future posts, I’ll dig into the magic which makes it all work.

Flicking through the Netflix Catalogue

The sample is a simple browser for the Netflix movie catalogue, which lets you scroll through all the available titles, or just those containing the search terms you enter. I chose this example because Netflix have an OData API which makes the catalogue very easy to query.

As well as the details view, shown below, the sample also has a card view which demonstrates our VirtualizingWrapPanel, also included in the sample.

Netflix Browser Sample

Click the image to see the sample in action. (There’s a known issue with images not showing up, because of how I’m hosting the files in Amazon S3 – run the sample locally to see it in its full glory)

Holding it all together: VirtualCollection

The key player in this setup is the VirtualCollection class, which you’ll find in the sample code. VirtualCollection acts as the data source which you bind to your ListBoxes or DataGrids, and it has the responsibility of coordinating data retrieval from the server.  The actual fetching of data is handled by a class implementing IVirtualCollectionSource: each VirtualCollection is associated with one IVirtualCollectionSource.

public class MainViewModel : ViewModel
{
   private NetflixTitlesSource _source;
   public VirtualCollection<Title> Items { get; private set; }

   public MainViewModel()
   {
       _source = new NetflixTitlesSource();
       Items = new VirtualCollection<Title>(_source, pageSize: 20, cachedPages: 5);
   }

   protected override void OnViewLoaded()
   {
       Items.Refresh();
   }
}

Using the VirtualCollection is very straightforward. You create an instance, telling it what kind of items it will be managing (Title objects in this case), and supplying it with the IVirtualCollectionSource (implemented here by NetflixTitlesSource – more on this in a moment).

You also tell it what page size to use, in other words, how many items it should fetch in each call to the server. You can determine an appropriate page size by figuring out how many items are likely to fill one screen of your application. VirtualCollection also wants to know how many such pages it should cache in memory: if your items are small, and change infrequently, caching more pages might well be a good idea.

Supplying the goods: IVirtualCollectionSource

When it comes to implementing IVirtualCollectionSource, you have to do a little more work, but nothing too arduous. In fact, there’s a base class which does much of the housekeeping for you.

An IVirtualCollectionSource has two main responsibilities: it needs to inform the VirtualCollection of the total number of items, and it must supply pages of items when requested. It should also let the VirtualCollection know if anything changes, by means of the CollectionChanged event.

Here’s what the key methods of the NetflixTitlesSource look like:

public class NetflixTitlesSource : VirtualCollectionSource<Title>
{
    private string _search;

   public string Search
   {
       get { return _search; }
       set
       {
           _search = value;
           Refresh(RefreshMode.ClearStaleData);
       }
   }
   
   protected override Task<int> GetCount()
   {
       return GetQueryResults(0, 1, null)
           .ContinueWith(t => (int)t.Result.TotalCount, TaskContinuationOptions.ExecuteSynchronously);
   }

   protected override Task<IList<Title>> GetPageAsyncOverride(int start, int pageSize, IList<SortDescription> sortDescriptions)
   {
       return GetQueryResults(start, pageSize, sortDescriptions)
           .ContinueWith(t => (IList<Title>)((IEnumerable<Title>)t.Result).ToList(), TaskContinuationOptions.ExecuteSynchronously);
   }

    private Task<QueryOperationResponse<Title>> GetQueryResults(int start, int pageSize, IList<SortDescription> sortDescriptions)
   {
       var context = new NetflixCatalog(new Uri("http://odata.netflix.com/Catalog"));

       var orderByString = CreateOrderByString(sortDescriptions);
       var query = context.Titles
           .AddQueryOption("$skip", start)
           .AddQueryOption("$top", pageSize)
           .IncludeTotalCount();

       if (!string.IsNullOrEmpty(Search))
       {
           query = query.AddQueryOption("$filter", "(substringof('" + Search + "',Name) eq true) and (BoxArt/SmallUrl ne null)");
       }
       else
       {
           query = query.AddQueryOption("$filter", "(BoxArt/SmallUrl ne null)");
       }

       if (orderByString.Length > 0)
       {
           query = query.AddQueryOption("$orderby", orderByString);
       }

       return Task.Factory.FromAsync<IEnumerable<Title>>(query.BeginExecute, query.EndExecute, null)
           .ContinueWith(t => (QueryOperationResponse<Title>)t.Result, TaskContinuationOptions.ExecuteSynchronously);
   }
   
   ...
}

As you can see, the majority of the work work lies in preparing the query that we send off to the Netflix OData API, and executing that asynchronously . By comparison, the code needed to support IVirtualCollectionSource (just two methods, GetCount and GetPageAsyncOverride) is very small.

A VirtualCollectionSource can communicate with its parent VirtualCollection by means of the CollectionChanged event if something happens that means the collection needs to be updated. In this case, changing the Search property is going to change the entire collection, so we call the Refresh method on the base class, which raises that event.

There are two kinds of Refresh available. The Search property is using the ClearStaleData mode, which means that the whole collection will be cleared whilst new results are being loaded. This is obviously the right choice here, because continuing to show results for “Harry Potter” after the user has just changed the search terms to “Indiana Jones” would be confusing.

The other kind of Refresh is called PermitStateDataWhilstRefreshing. This mode is good if your server has just notified you that new items have been added. Existing items are still valid, so there’s no need to clear down the whole collection whilst you find out about the new items.

Making it Presentable

Finally, here’s some XAML to show how we might display the contents of the VirtualCollection in a DataGrid

<sdk:DataGrid ItemsSource="{Binding Items}" Grid.Row="1" Margin="10,0" IsReadOnly="True" AutoGenerateColumns="False" RowHeight="50">
 <i:Interaction.Behaviors>
     <VirtualCollection1:ProvideVisibleItemRangeFromDataGridBehavior/>
 </i:Interaction.Behaviors>
 <sdk:DataGrid.Columns>
     <sdk:DataGridTemplateColumn Header="Image">
         <sdk:DataGridTemplateColumn.CellTemplate>
             <DataTemplate>
                 <Image Stretch="None" 
                         Source="{Binding Item.BoxArt.SmallUrl, Converter={StaticResource Converter_StringToImage}}" 
                        Margin="5" />
             </DataTemplate>
         </sdk:DataGridTemplateColumn.CellTemplate>
     </sdk:DataGridTemplateColumn>
     <sdk:DataGridTextColumn Binding="{Binding Item.Name}" Header="Name" CanUserSort="True" SortMemberPath="Name"/>
     <sdk:DataGridTextColumn Binding="{Binding Item.AverageRating}" Header="Average Rating" CanUserSort="True" SortMemberPath="AverageRating"/>
     <sdk:DataGridTextColumn Binding="{Binding Item.ReleaseYear}" Header="Release Year" CanUserSort="True" SortMemberPath="ReleaseYear"/>
 </sdk:DataGrid.Columns>
</sdk:DataGrid>

The main thing to notice here is that all the binding paths start with Item. That’s because each data item in the VirtualCollection is wrapped in a VirtualItem, and made available through VirtualItem’s Item property. VirtualCollection does this so that it can put off fetching any actual items until the last possible moment – more on this in another blog post.

Notice also that we can enable sorting on some of the columns by setting a SortMemberPath. When you click a column header, the DataGrid will pass that value to the VirtualCollection, and VirtualCollection will pass it on to the IVirtualCollectionSource which can then use it to instruct the server in how to sort the results it returns.

Try it out and Get Back to Us

Now you’ve seen how easy it is, you have no excuse. Go and purge those paging controls from your apps! Long live scrollbars!

All the code you need is up on GitHub - look in the VirtualCollections folder. And don’t forget to check back here shortly, as we take a look behind the scenes to find out how VirtualCollection works.

We’d love to hear how you get on with this in your own projects.

Comments

tobi
08/14/2012 09:14 PM by
tobi

"substringof('" + Search + "',Name)"

Ohhh injection! ;-)

Comments have been closed on this topic.