WCF RIA: Connecting DataPager with Silverlight DataGrid to perform optimized paging on sever side.
Silverlight and WCF RIA Services are aimed to provide easy CRUD operations. And this is often the case. One problem I have been facing is paging in DataGrid + DataPager. Generally all works well, but there might be the case when you need to optimize this one.
What we aim to do is having paging support without writing any custom code on the client(silverlight). We can bind DataPager by this command
DataGrid binds to the same DataSource
DataSource will have SortDescriptor set to appropriate column.
As a result we will have paging functionality without any significant coding. However one element is missing. As you may notice DataPagers's TotalNumberOfItems equals -1. First thought is to set this property DataPager.TotalNumberOfItems to correct value. But unfortunately it is read-only property... One way to overcome this is to override Count() property in our DomainService. This will work "magically" and our DataPager now will get appropriate value. Great.
But another question remains open. How we can get exact range in our DomainService without creating additional parameters? The reason why we might need this does not always looks obvious.
Let's have a look what request does silverlight initiate by using Fiddler.
http://..../AmazonService.svc/binary/GetAllAmazonOrders1000Heavy?$orderby=it.OrderId&$skip=10&$take=10&$includeTotalCount=
RIA Services [Query] should return all list of objects and only then they are "skipped" and "taken" by RIA Services. But forming all list of objects might take time. This is true if you need to return heavy objects like associations, blob data, etc. So our goal is to optimize it and one way to do that is to know exactly the range of object we need to fetch with accurate data. Other range does not needs to be initialized so we can fill that with empty objects, as they are to be skipped by RIA Query mechanism in any case.
The only way I can see is to analyse HttpContext.Current.Request.Headers.
So you can get "$skip" and "$take" parameters from there. And knowing this it is not hard to define the range we must focus on.
What we aim to do is having paging support without writing any custom code on the client(silverlight). We can bind DataPager by this command
<sdk:DataPager Height="26" HorizontalAlignment="Left" Margin="8,54,0,0" Name="dataPager1" PageSize="10" VerticalAlignment="Top" Width="213" Source="{Binding Data, ElementName=amazonOrderDomainDataSource}" DisplayMode="FirstLastPreviousNext" TabNavigation="Local"
DataGrid binds to the same DataSource
<sdk:DataGrid AutoGenerateColumns="False" Height="195" HorizontalAlignment="Left" ItemsSource="{Binding Data, ElementName=amazonOrderDomainDataSource}"
DataSource will have SortDescriptor set to appropriate column.
<riaControls:DomainDataSource AutoLoad="True" d:DesignData="{d:DesignInstance my:AmazonOrder, CreateList=true}" Height="0" LoadSize="10" LoadedData="amazonOrderDomainDataSource_LoadedData" Name="amazonOrderDomainDataSource" QueryName="GetAllAmazonOrders1000HeavyQuery" Width="0"> <riaControls:DomainDataSource.SortDescriptors> <riaControls:SortDescriptor PropertyPath="OrderId" /> riaControls:DomainDataSource.SortDescriptors> <riaControls:DomainDataSource.DomainContext> <my1:AmazonContext /> riaControls:DomainDataSource.DomainContext> riaControls:DomainDataSource>
As a result we will have paging functionality without any significant coding. However one element is missing. As you may notice DataPagers's TotalNumberOfItems equals -1. First thought is to set this property DataPager.TotalNumberOfItems to correct value. But unfortunately it is read-only property... One way to overcome this is to override Count()
//This method is needed in order to return total number of items(for DataPager) protected override int Count( IQueryablequery) { var count = 0; try { count = query.Count(); } catch (Exception) { count = -1; //// not known } return count; }
But another question remains open. How we can get exact range in our DomainService without creating additional parameters? The reason why we might need this does not always looks obvious.
Let's have a look what request does silverlight initiate by using Fiddler.
http://..../AmazonService.svc/binary/GetAllAmazonOrders1000Heavy?$orderby=it.OrderId&$skip=10&$take=10&$includeTotalCount=
RIA Services [Query] should return all list of objects and only then they are "skipped" and "taken" by RIA Services. But forming all list of objects might take time. This is true if you need to return heavy objects like associations, blob data, etc. So our goal is to optimize it and one way to do that is to know exactly the range of object we need to fetch with accurate data. Other range does not needs to be initialized so we can fill that with empty objects, as they are to be skipped by RIA Query mechanism in any case.
The only way I can see is to analyse HttpContext.Current.Request.Headers.
////// This is helper class for WCF RIA Services + Silverlight DataPager. /// Silverlight DataPager supports data paging. It makes request with parameters $skip, $take. The only problem is that is inaccessible in our Query method. /// This class provides access to these fields so it would be possible to analyze do we need to init data or just skip. /// public class DataPagingHelper { protected int SkipInt = 0; protected int TakeInt = 0; public int TakeFromInclusive = 0; public int TakeToInclusive = Int32.MaxValue; /// /// Analyzes request and tells if request was likely made from silverlight data pager. /// public bool IsCalledFromDataPager { get { return (TakeInt > 0); } } /// /// Determines if we need to load data for particular index in list /// /// "iCurrent">Current index of list being analyzed /// Boolean public bool CurrentIndexInOurRange(int iCurrent) { return ((iCurrent >= this.TakeFromInclusive) && (iCurrent <= this.TakeToInclusive)); } private void Init(NameValueCollection parameters) { SkipInt = 0; TakeInt = 0; var skipStr = parameters.Get("$skip"); if (skipStr != null) { if (int.TryParse(skipStr, out SkipInt)) { TakeFromInclusive = SkipInt; } } var takeStr = parameters.Get("$take"); if (takeStr != null) { if (int.TryParse(takeStr, out TakeInt)) { TakeToInclusive = SkipInt + TakeInt; } } } public DataPagingHelper(NameValueCollection parameters) { this.Init(parameters); } public DataPagingHelper(HttpRequest httpRequest) { this.Init(httpRequest.Params); } }
So you can get "$skip" and "$take" parameters from there. And knowing this it is not hard to define the range we must focus on.
[Query] public IQueryable<AmazonOrder> GetAllAmazonOrders1000Heavy() { var dataPagingHelper = new DataPagingHelper(HttpContext.Current.Request); var orders = _amazonBooksServiceClient.GetAllAmazonOrdersHuge(); //Besides that we have to define associations we actually need to fill in data if we want associated properties to be //transferred to the client application. int iCurrent = 1; foreach (AmazonOrder amazonOrder in orders) { if (dataPagingHelper.CurrentIndexInOurRange(iCurrent)) //do we need to return details? { //time-consuming operations... AmazonOrder order = amazonOrder; amazonOrder.AmazonBook = _amazonBooksServiceClient.GetAmazonBook(order.BookId); amazonOrder.AmazonCustomer = _amazonBooksServiceClient.GetAmazonCustomer(order.CustomerId); } else { if (iCurrent > dataPagingHelper.TakeToInclusive) { break; //no further need to init objects that we won't use } } iCurrent++; } return orders.AsQueryable(); }
Comments
Post a Comment