WPF: a generic ‘server-side’ pagination data provider

Print Content | More

Sooner or later everyone face the problem of having to deal with paginated data in WPF, paginating a collection is quite easy and you can refer to one of my previous posts too (http://www.primordialcode.com/index.php/2008/09/08/silverlight-a-generic-pagination-control/ and http://www.primordialcode.com/index.php/2008/09/19/silverlight-pagination-control-bug-fixed/).

This time I needed a way to interact with some WCF services that I have to use in order to get the paged data from the server. What I wanted to create was a component that encapsulates the logic of handling the requests for the data in order to minimize the amount of code I have to write for similar operations in different forms of the application, some key features:

  • It has to handle a reference to the current page index
  • You have to specify the page size
  • You have to specify the types of object that will be hold inside the returned collection
  • You can specify an object that represent a set of filter you can do on the data (you can change them and ask for a refresh)
  • You have to specify the function that will be used to retrieve the paged data, it has however to conform to a specific signature.
  • You can use the data structures returned by this object (CurrentPage, TotalPages and Data, which contains the actual array of data objects) with the binding infrastructure of WPF.
  • This control does not have a graphical representation for the command buttons (First, Last, Next, etc.), it is only a support class.

Here’s the actual implementation:

/// <summary>
/// Starting point for a WPF paged data source control, next step: convert it to a real data source control
/// </summary>
/// <typeparam name="T">Types of object that will be returned</typeparam>
/// <typeparam name="TParams">Type of the class that holds optional parameters data</typeparam>
public class PagedObjectDataProvider<T, TParams> : INotifyPropertyChanged
{
   /// <summary>
   /// defines the signature of the function that will be used to retrieve the data
   /// </summary>
   /// <param name="pageindex">index of the page requested</param>
   /// <param name="pagesize">size of the page</param>
   /// <param name="param">optional parameters to be passed to the function</param>
   /// <param name="numrows">on exit it will contains the total number of records available to be retrieve from the source</param>
   /// <returns></returns>
   public delegate IList<T> GetPagedDataDelegate(int pageindex, int pagesize, TParams param, out int numrows);
 
   /// <summary>
   /// Current page index
   /// </summary>
   public int CurrentPage
   {
      get { return _CurrentPage + 1; }
      set { _CurrentPage = value - 1; }
   }
   private int _CurrentPage;
   
   /// <summary>
   /// Size of the page
   /// </summary>
   public int PageSize { get; set; }
   
   /// <summary>
   /// Total items available on the server to retrieve
   /// </summary>
   public int TotalRowsCount
   {
      get { return _TotalRowCount; }
      set { _TotalRowCount = value; }
   }
   private int _TotalRowCount;
   
   /// <summary>
   /// Total number of pages
   /// </summary>
   public int TotalPagesCount
   {
      get
      {
         int totalpages = _TotalRowCount / PageSize;
         totalpages += ((_TotalRowCount % PageSize) > 0) ? 1 : 0;
         return totalpages;
      }
   }
 
   /// <summary>
   /// Parameters that will be used as a 'filter' by the function that requests data
   /// </summary>
   public TParams Parameters { get; set; }
 
   private GetPagedDataDelegate GetDataFunction;
 
   /// <summary>
   /// This member property actually holds the data and you can bind to it
   /// </summary>
   public ObservableCollection<T> Data { get; internal set; }
 
   public PagedObjectDataProvider(int pageSize, TParams parameters, GetPagedDataDelegate getDataFunction)
   {
      PageSize = pageSize;
      Parameters = parameters;
      GetDataFunction = getDataFunction;
      Data = new ObservableCollection<T>();
      _CurrentPage = -1;
   }
 
   public void GetData()
   {
      // the first time we always get the first page if not initialized in any way
      // execute this function in a background thread to not block the interface
      if (_CurrentPage < 0)
         _CurrentPage = 0;
      
      Data.Clear();
      // execute this in a background thread to emulate the IsAsyncronous behavior of the objectdatasource
      ThreadPool.QueueUserWorkItem(new WaitCallback(InternalGetData), null);
   }
 
   private void InternalGetData(object addictionaldata)
   {
      GetDataFunction(_CurrentPage, PageSize, Parameters, out _TotalRowCount).ForEach(o => UiThread.Run(() => Data.Add(o)));
      if (PropertyChanged != null)
      {
         PropertyChanged(this, new PropertyChangedEventArgs("CurrentPage"));
         PropertyChanged(this, new PropertyChangedEventArgs("TotalRowsCount"));
         PropertyChanged(this, new PropertyChangedEventArgs("TotalPagesCount"));
         PropertyChanged(this, new PropertyChangedEventArgs("Data"));
      }
   }
 
   /// <summary>
   /// Function used to request the next page of data
   /// </summary>
   /// <param name="page"></param>
   /// <returns></returns>
   public ICollection<T> MoveTo(PageMode page)
   {
      if (_CurrentPage >= 0)
         switch (page)
         {
            case PageMode.First:
               _CurrentPage = 0;
               break;
            case PageMode.Previous:
               _CurrentPage = Math.Max(_CurrentPage - 1, 0);
               break;
            case PageMode.Next:
               _CurrentPage = Math.Min(_CurrentPage + 1, TotalPagesCount - 1);
               break;
            case PageMode.Last:
               _CurrentPage = TotalPagesCount - 1;
               break;
         }
 
      GetData();
 
      return Data;
   }
 
   #region INotifyPropertyChanged Members
 
   public event PropertyChangedEventHandler PropertyChanged;
 
   #endregion
}
 
public enum PageMode
{
   First,
   Previous,
   Next,
   Last
}

To use it you need to create it in code, with something like:

// define a property at the class level
public PagedObjectDataProvider<LinkResultDto, object> RilevazioniPaged { get; internal set; }
 
...
 
// define a function that will be used to retrieve the data
private IList<LinkResultDto> GetPagedDataDelegate(int pageindex, int pagesize, object param, out int numrows)
        {
            return RilevazioniService.GetRilevazioniForBrowser(11, null, 0, 0, null, pageindex, pagesize, out numrows);
        }
 
...
 
// create the object in the constructor
RilevazioniPaged = new PagedObjectDataProvider<LinkResultDto, object>(20, null, GetPagedDataDelegate);
 
...
 
// Call the MoveTo function to retrieve the data from the service
RilevazioniPaged.MoveTo(mode);

To display the results you can use a Xaml like this:

<!-- data section -->
<ListView Grid.Row="0" Grid.Column="0" 
          Name="lvRilevazioni" ItemsSource="{Binding RilevazioniPaged.Data}" SelectionMode="Single"
          IsSynchronizedWithCurrentItem="True" HorizontalContentAlignment="Stretch" 
          VerticalAlignment="Stretch"  >
</ListView>
...
<!-- Pager section -->
<StackPanel Orientation="Horizontal">
    <Button Content="First" Click="First_Click" />
    <Button Content="Previous" Click="Previous_Click" />
    <Label>Page:</Label>
    <Label Content="{Binding RilevazioniPaged.CurrentPage}" />
    <Label>/</Label>
    <Label Content="{Binding RilevazioniPaged.TotalPagesCount}" />
    <Button Content="Next" Click="Next_Click" />
    <Button Content="Last" Click="Last_Click" />
</StackPanel>

This is a ‘first-shot’ solution and it doesn’t totally satisfies me, I’ll work to modify it in the future and have something more similar to a ‘true’ WPF data provider that you can use declaratively in the designer too.



Data provider, Pagination, Wpf

1 comments

Related Post

  1. #1 da Umang Patel - Thursday September 2010 alle 05:21

    Thanks for the good explanation!!!

    It helps me to implement server side pagination in my Silverlight Application.

All fields are required and you must provide valid data in order to be able to comment on this post.


(will not be published)
(es: http://www.mysite.com)