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.
Related Content
- WPF: x:Name Referenced Objects (06/01/2010)
- Silverlight Pagination Control – bug fixed (26/08/2015)
- NHibernate - Eager Fetch and Pagination in one query (26/08/2015)
- WPF, Silverlight, WP7 and the Async CTP (26/08/2015)
- Silverlight: a generic Pagination Control (09/08/2008)
- More related document (15)