Quick Summary The VirtualCollection class uses the ItemDataRequested event to fetch data for the collection Using a wrapper class which includes the requested collection along with the next “start index” and the total number of records helps simplify paged data binding
Enabling client-side paging for the xamWebGrid is as simple as enabling the behavior on the grid. Often, however, data sets are larger than what is desirable to send to the client for in-memory paging - so delegating paging to the server is preferred.
A common approach for dealing with server-side paging is to handle the PageIndexChanged event of a grid and then send this information to the data service for the resulting records. While using the concept of page indices is common under normal circumstances, when using the Infragistics VirtualCollection class your application is concerned with tracking the last index in the collection needing data, rather than a current page index. The VirtualCollection implements IPagedCollectionView which allows the collection to provide automatic paging capabilities.
Consider the following diagram to illustrate the architecture of the example implemented in this article:
The user control hosts a xamWebGrid which is bound to a ViewModel with an instance of the VirtualCollection. When the grid attempts to display data, the VirtualCollection is contacted for the items in it’s cache. As soon as the VirtualCollection recognizes that there is no data in the index position requested by the consuming control (in this case the grid) the the ItemDataRequested event is fired and then the application has an opportunity to contact the server for data.
On the server the data is queried and then returned to the client using a DataPage class that encapsulates the query result, the index being tracked by the collection and the total number of items in the query. This information is returned to the client and data binding executes as expected.
To begin a detailed discussion of this example we’ll first examine the data service.
The service used in this application is will feed the client with needed data to fill the VirtualCollection. Creating the service is a simple matter of adding a new WCF Service to the solution. In this application, it’s named NorthwindService.svc.
When you add a service to the project a service file and interface are created. The following code listing represents the service’s interface:
using System.ServiceModel; using na2010v1.Web.Model; namespace na2010v1.Web.Services { [ServiceContract] public interface INorthwind { [OperationContract] ProductDTODataPage GetProductsForPaging(int pageSize, int startIndex, int resultTotalCount); } }
The concrete class that implements the INorthwind interface is below:
using System.Collections.Generic; using System.Linq; using System.ServiceModel.Activation; using na2010v1.Web.Model; namespace na2010v1.Web.Services { [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)] public class Northwind : INorthwind { public ProductDTODataPage GetProductsForPaging(int pageSize, int startIndex, int resultTotalCount) { IList<ProductDTO> list = null; using (NorthwindDataContext db = new NorthwindDataContext()) { var products = (from p in db.Products select new ProductDTO() { ProductID = p.ProductID, ProductName = p.ProductName, UnitPrice = p.UnitPrice }).Skip(startIndex).Take(pageSize); if (resultTotalCount == 0) { resultTotalCount = (from p in db.Products select p).Count(); } list = products.ToList<ProductDTO>(); } ProductDTODataPage page = new ProductDTODataPage(startIndex, list, resultTotalCount); return page; } } }
This service uses a Linq to SQL data context to query the database for all the records in the products table. A projection to the ProductsDTO class is required in this instance in order to avoid serialization issues against the Linq to SQL models. If the projection is not used, the service would try to serialize the Linq to SQL models and the objects attempt to lazy load related data into the object graph causing the serialization errors. To avoid these types of errors the following data transfer object class is used to ship data to the client:
using System.Runtime.Serialization; namespace na2010v1.Web.Model { [DataContractAttribute] public class ProductDTO { [DataMember] public int ProductID { get; set; } [DataMember] public string ProductName { get; set; } [DataMember] public decimal? UnitPrice { get; set; } } }
Notice the arguments for GetProductsForPaging. Just as you might expect this method needs to know the index in which to begin the paged query, and the page size, but an argument for the total number of records in the query isn’t something you normally see. In this example, the total number of items is cached upon getting a response from the server and then passed back to the server upon subsequent requests. This approach is favorable (in this example at least) so that the client does not have to query the server for the total count independently. Since the calls to the server are asynchronous you would have to write some interesting code to ensure you have not only the query results, but also the total count for the query are available before the grid could bind to the collection. With this approach the service will only query for the total number of records if the total count passed to it is zero, otherwise the count query is skipped. In the end using this technique will make all the necessary data available at the right time for the xamWebGrid.
The response from this method comes in the form of a wrapper class that encapsulates the result set along with the total items count and the current item index. This class is derived from the DataPage class.
Often when querying data from a service, developers will choose to return a collection of generic IEnumerable or perhaps IList collections. While these constructs will transport the data result well, in this case the application needs to know a little more information about the query, which is why the generic DataPage class is needed:
using System.Collections.Generic; using System.Runtime.Serialization; namespace na2010v1.Web.Model { [DataContract] public class DataPage<T> { [DataMember] public IList<T> DataItems { get; set; } [DataMember] public int StartIndex { get; set; } [DataMember] public int TotalItemsCount { get; set; } public DataPage(int index, IList<T> items, int totalItemsCount) { this.StartIndex = index; this.DataItems = items; this.TotalItemsCount = totalItemsCount; } } }
The DataPage encapsulates the result set along with the start index and total items count. The start index represents the index value where the application needs to bind or query from next.
Example: The first time data is requested the StartIndex will have a value of 0. Assuming a load size of 10, then the next query will find the StartIndex having a value of 10. This is because records at index positions 0-9 are loaded on the first request and records at position 10-19 are returned from the second query, and so on...
When exposing a generic class as a service reference, the proxy that is created on the client must create a unique class name in order to resolve the generic nature of the class – there are no <T> classes in generated proxies. The following screenshot demonstrates the kind of random class name generated by the client:
In order to work around this there are at last two options – you may choose to not use generic classes at all, or subclass the generic DataPage class. Subclassing the generic base calls allows the client to generate a friendly name for the proxy. Abandoning generics would create a lot of extra code simply for the sake of implementing the same wrapper functionality for each type – therefore the approach used in this application is to inherit from the generic DataPage class. Using inheritance will type a class for the purposes of the query and leave the implementation details to the super class:
using System.Collections.Generic; using System.Runtime.Serialization; namespace na2010v1.Web.Model { [DataContractAttribute] public class ProductDTODataPage : DataPage<ProductDTO> { public ProductDTODataPage(int index, IList<ProductDTO> items, int totalItemsCount) : base(index, items, totalItemsCount) { } } }
Now that you’ve seen how the service is implemented you can turn your attention to the view.
This example includes two controls, a BusyIndicator (available in the Silverlight toolkit) and xamWebGrid. While the page is querying for data, the busy indicator displays a progress bar. The following screenshots depict how the page looks before and after the data is loaded:
xmlns:igGrid="clr-namespace:Infragistics.Silverlight.Controls;assembly=Infragistics.Silverlight.XamWebGrid.v10.1" xmlns:controlsToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit" ... <Grid x:Name="LayoutRoot" Height="320"> <igGrid:XamWebGrid x:Name="grid" ItemsSource="{Binding Items}"> <igGrid:XamWebGrid.PagerSettings> <igGrid:PagerSettings PageSize="10" AllowPaging="Bottom" /> </igGrid:XamWebGrid.PagerSettings> </igGrid:XamWebGrid> <controlsToolkit:BusyIndicator Canvas.ZIndex="100" x:Name="BusyIndicator" Canvas.Left="300" Canvas.Top="100" BusyContent="{Binding BusyText}" IsBusy="{Binding IsLoading}"/> </Grid>
The xamWebGrid and BusyIndicator are placed on the page. The gird is binding it’s ItemsSource property to the ViewModel’s Items member, and the busy indicator is bound to properties on the ViewModel to display custom text to the user and show and hide the control on the page.
Minimal code is necessary in the code behind to link the ViewModel to the View.
using System.Windows; using System.Windows.Controls; namespace na2010v1.Views.xamWebGrid.VirtualCollectionPaging.test { public partial class VirtualCollectionPaging : Page { private VirtualCollectionPagingViewModel _vm; public VirtualCollectionPaging() { InitializeComponent(); this.Loaded += new RoutedEventHandler(VirtualCollectionPaging_Loaded); } void VirtualCollectionPaging_Loaded(object sender, RoutedEventArgs e) { this._vm = new VirtualCollectionPagingViewModel(this.grid.PagerSettings.PageSize); this.DataContext = this._vm; } } }
The ViewModel is responsible for requesting data from the server and driving the properties bound to elements on the View.
In order to access the NorthwindService you must first add a new service reference to the Silverlight project.
Note: Make sure to click on the Discover button to find exposed services within your project.
While the following code listing is a bit long, much of what you see is boilerplate code required to support INotifyPropertyChanged (which is explicitly implemented in the base class ViewModelBase). The most interesting parts of the View Model lie in the _items_ItemDataRequested and _client_GetProductsForPagingCompleted methods.
using System; using Infragistics.Silverlight; using System.Collections.Generic; using na2010v1.NorthwindService; using na2010v1.Model; namespace na2010v1.Views.xamWebGrid.VirtualCollectionPaging { public class VirtualCollectionPagingViewModel : ViewModelBase { private VirtualCollection<ProductDTO> _items = null; private NorthwindClient _client = null; private int _resultTotalCount = 0; private bool _isLoading = true; private string _busyText = "Loading..."; public VirtualCollection<ProductDTO> Items { get { return this._items; } } public bool IsLoading { get { return this._isLoading; } private set { if (this._isLoading != value) { this._isLoading = value; this.RaisePropertyChangedEvent("IsLoading"); } } } public string BusyText { get { return this._busyText; } private set { if (this._busyText != value) { this._busyText = value; this.RaisePropertyChangedEvent("BusyText"); } } } public VirtualCollectionPagingViewModel(int pageSize) { this._items = new VirtualCollection<ProductDTO>(100, pageSize); this._items.PageSize = pageSize; this._items.ItemDataRequested += new EventHandler<ItemDataRequestedEventArgs>(_items_ItemDataRequested); this._client = new NorthwindClient(); this._client.GetProductsForPagingCompleted += new EventHandler<GetProductsForPagingCompletedEventArgs>(_client_GetProductsForPagingCompleted); } private void _items_ItemDataRequested(object sender, ItemDataRequestedEventArgs e) { this.NotifyLoadStart("Loading Products..."); this._client.GetProductsForPagingAsync(this._items.PageSize, e.StartIndex, this._resultTotalCount); } private void _client_GetProductsForPagingCompleted(object sender, GetProductsForPagingCompletedEventArgs e) { this._resultTotalCount = e.Result.TotalItemsCount; this._items.AnticipatedCount = this._resultTotalCount; this._items.LoadItems(e.Result.StartIndex, e.Result.DataItems); this.NotifyLoadStop(); } private void NotifyLoadStart(string message) { this.IsLoading = true; this.BusyText = message; } private void NotifyLoadStop() { this.IsLoading = false; } } }
Beginning with the ViewModel’s constructor, notice how the member _items is instantiated as a new VirtualCollection. The arguments being passed into the VirtualCollection represent the anticipated size of the entire set being managed by the collection and the load size. The load size is the amount of records that are requested by a data service when the collection needs additional data. The anticipated size will tell the collection in general terms how many records to expect to be working with throughout it’s lifecycle. This number becomes important in a paging scenario because the anticipated amount is the value the paging behavior of the xamWebGrid uses to calculate the number of pages to expose to the user. In the end, the collection’s anticipated size is set to the count of the of total number of records in the query in scope, but the actual value is retrieved when the client fetches data for the collection.
The reminder of the constructor's code is concerned with instantiating the client to access the sever and subscribing to the requisite events. In this case the “client” is a WCF service that exposes the Northwind database.
The next method, _items_ItemDataRequested fires when the VirtualCollection attempts to display data that has yet to be returned from the server. When the grid is first loaded on the page, the VirtualCollection will not have any data, then the ItemDataRequested event will fire prompting the application to contact the server for data to partially fill the collection. The next time the collection attempts to render data in a location in the collection that does not have data, ItemsDataRequested will fire once again allowing the collection to receive the next set of data.
The Northwind WCF service exposes a method GetProductsForPagingAsync which will return paged sets of data from the Northwind Products table. When a response from the server is received, the _client_GetProductsForPagingCompleted method fires.
When the user control gets the response from the server the VirtualCollection is loaded with the latest paged set of data. The first line in the _client_GetProductsForPagingCompleted method takes the value of e.Result.TotalItemsCount and caches it in the _resultTotalCount variable. Next, the VirtualCollection’s anticipated size is updated from the best guess passed in to the constructor to the actual number of items in the query. Finally the VirtualCollection’s Load method is used to fill the collection with the latest data, starting at the index as tracked by e.Result.StartIndex.
Thanks to Marin Marinov on the Infragistics Silverlight Engineering team for help with this article.