Paging Control in C#


Problem

There is no paging support in DataGrid/ListBox in WPF.

Introduction

I have been working with WPF for the last 3 years. Initially there was no DataGrid control in WPF. Microsoft introduced it in .Net Framework 4. So, we used a ListBox as a DataGrid by applying a control template. In my project, we have a massive amount of data. We have to retrieve data in chunks (pages). For the same, we have to implement UI logic + Data fetching logic for every page for data retrieval. It was a very tedious task. So, I decided to make a generic control that can handle paging.

Overview

Rather than adding paging functionality to a DataGrid, I came up with another idea; to make paging a separate control. The Paging-Control will take care of the page retrieval task. The developer has only to bind the DataGrid or ListBox to the ItemsSource provided by the Paging-Control.

Paging1.gifPaging2.gif

So, it's kind of a plug & play system. You can plug any control capable of displaying a ICollection<T> to PagingControl without code. You just have to implement an IpageContract interface. This interface contains only two methods, one for getting the count and the other for getting Data.

In this very first article, I have only covered fetching data in chunks (pages) without any filter or searching criteria. I'll cover that in a subsequent article.

Implementation Details

[TemplatePart(Name = "PART_FirstPageButton", Type = typeof(Button)),
 TemplatePart(Name = "PART_PreviousPageButton", Type = typeof(Button)),
 TemplatePart(Name = "PART_PageTextBox", Type = typeof(TextBox)),
 TemplatePart(Name = "PART_NextPageButton", Type = typeof(Button)),
 TemplatePart(Name = "PART_LastPageButton", Type = typeof(Button)),
 TemplatePart(Name = "PART_PageSizesCombobox", Type = typeof(ComboBox))]
 public class PaggingControl : Control
 {
………
 }


I have used a TemplatePart in my paging control. It is a single .CS file inheriting from the Control class. Here I have used 4 Buttons for navigation, 1 textbox to display a current page or set page manually and one combobox to set page size. I used a TemplatePart to give freedom to other developer to completely change the UI of this control and make it simple to use.

I have created the following dependency properties and relevant simple property for binding.

public static readonly DependencyProperty ItemsSourceProperty;
public static readonly DependencyProperty PageProperty;
public static readonly DependencyProperty TotalPagesProperty;
public static readonly DependencyProperty PageSizesProperty;
public static readonly DependencyProperty PageContractProperty;
public static readonly DependencyProperty FilterTagProperty;

public ObservableCollection<object> ItemsSource
public uint Page
public uint TotalPages
public ObservableCollection<uint> PageSizes
public IPageControlContract PageContract
public object FilterTag

I have created two RoutedEvent for the page change event; one gets fired before changing the page and the other after changing the page.

public delegate void PageChangedEventHandler(object sender, PageChangedEventArgs args);

public
static readonly RoutedEvent PreviewPageChangeEvent;
public static readonly RoutedEvent PageChangedEvent;

public
event PageChangedEventHandler PreviewPageChange
public event PageChangedEventHandler PageChanged


We have overridden the OnApplyTemplate methods. By doing so, we'll fetch all the child-control reference to local variables, so that we can refer to them throughout the control. We also make sure that none of them is missing. If any one of them is missing, then we'll throw an exception.

public override void OnApplyTemplate()
{
    btnFirstPage = this.Template.FindName("PART_FirstPageButton", this) as Button;
    btnPreviousPage = this.Template.FindName("PART_PreviousPageButton", this) as Button;
    txtPage = this.Template.FindName("PART_PageTextBox", this) as TextBox;
    btnNextPage = this.Template.FindName("PART_NextPageButton", this) as Button;
    btnLastPage = this.Template.FindName("PART_LastPageButton", this) as Button;
    cmbPageSizes = this.Template.FindName("PART_PageSizesCombobox", this) as ComboBox;

            if (btnFirstPage == null ||
          btnPreviousPage == null ||
          txtPage == null ||
          btnNextPage == null ||
          btnLastPage == null ||
          cmbPageSizes == null)
          {
             throw new Exception("Invalid Control template.");
          }

      base.OnApplyTemplate();
}


Once a control has been loaded, we start our work.

 void PaggingControl_Loaded(object sender, RoutedEventArgs e)
 {
     if (Template == null)
     {
         throw new Exception("Control template not assigned.");
     }

     if (PageContract == null)
     {
         throw new Exception("IPageControlContract not assigned.");
     }

     RegisterEvents();
     SetDefaultValues();
     BindProperties();
 }

In the above code, we first check, whether the control template has been applied to the PagingControl or not. After checking the Template, we go for the PageContract. We check if the PageContract has been assigned or not. This contract is important because all the data retrieval work is done by this PageContract instance.

The RegisterEvents method does all the events registeration work.

private void RegisterEvents()
{
    btnFirstPage.Click += new RoutedEventHandler(btnFirstPage_Click);
    btnPreviousPage.Click += new RoutedEventHandler(btnPreviousPage_Click);
    btnNextPage.Click += new RoutedEventHandler(btnNextPage_Click);
    btnLastPage.Click += new RoutedEventHandler(btnLastPage_Click);

    txtPage.LostFocus += new RoutedEventHandler(txtPage_LostFocus);

    cmbPageSizes.SelectionChanged += new   SelectionChangedEventHandler(cmbPageSizes_SelectionChanged);
}

The SetDefaultValues method will initialize local variable properties to the appropriate default values.

private void SetDefaultValues()
{
    ItemsSource = new ObservableCollection<object>();

    cmbPageSizes.IsEditable = false;
    cmbPageSizes.SelectedIndex = 0;
}


BindProperties will do binding of properties. Here, we have bound a Page property to a textbox supplied to PageControl by the control template. The same for the PageSizes property - Combobox control.

private void BindProperties()
{
    Binding propBinding;

    propBinding = new Binding("Page");
    propBinding.RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent);
    propBinding.Mode = BindingMode.TwoWay;
    propBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
    txtPage.SetBinding(TextBox.TextProperty, propBinding);

    propBinding = new Binding("PageSizes");
    propBinding.RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent);
    propBinding.Mode = BindingMode.TwoWay;
    propBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
    cmbPageSizes.SetBinding(ComboBox.ItemsSourceProperty, propBinding);
}


Now, we're done with setting up the control. As we have kept SelectedIndex=0 in combobox, on finishing loading, Combobox selection is changed. So, the item change event will be fired. So the control will start loading data.

void cmbPageSizes_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    Navigate(PageChanges.Current);
}


The preceing event will call a private method with navigation type. It's an enum. It is defined as below.

internal enum PageChanges
{
    First, //FOR FIRST BUTTON
    Previous, //FOR PREVIOUS BUTTON
    Current, //FOR COMBOBOX ITEM CHANGE EVENT AND PAGE TEXT LOST FOCUS
    Next, //FOR NEXT BUTTON
    Last //FOR LAST BUTTON
}


This navigate method is called from all the 6 registered events with an appropriate enum value. This method contains the core logic of the paging control.

private void Navigate(PageChanges change)
{
    uint totalRecords;
    uint newPageSize;

    if (PageContract == null) //IF NO CONTRACT THEN RETURN
    {
        return;
    }

    totalRecords = PageContract.GetTotalCount();
    //GETTING NEW TOTAL RECORDS COUNT
    newPageSize = (uint)cmbPageSizes.SelectedItem;
   
//GETTING NEW PAGE SIZE

    if (totalRecords == 0)
    {
        //IF NO RECORD FOUND, THEN CLEAR ITEMSSOURCE
        ItemsSource.Clear();
        TotalPages = 1;
        Page = 1;
    }
    else
    {
        //CALCULATE TOTALPAGES
        TotalPages = (totalRecords / newPageSize) + (uint)((totalRecords % newPageSize == 0) ? 0 : 1);
}
uint newPage = 1;

//SETTING NEW PAGE VARIABLE BASED ON CHANGE ENUM
//FOLLOWING SWITCH CODE IS SELF-EXPLANATORY
switch (change)
{
    case PageChanges.First:
        if (Page == 1)
        {
            return;
        }
        break;
    case PageChanges.Previous:
        newPage = (Page - 1 > TotalPages) ? TotalPages : (Page - 1 < 1) ? 1 : Page - 1;
        break;
    case PageChanges.Current:
        newPage = (Page > TotalPages) ? TotalPages : (Page < 1) ? 1 : Page;
        break;
    case PageChanges.Next:
        newPage = (Page + 1 > TotalPages) ? TotalPages : Page + 1;

        break;
    case PageChanges.Last:
        if (Page == TotalPages)
        {
            return;
        }
        newPage = TotalPages;
        break;
    default:
        break;
}

//BASED ON NEW PAGE SIZE, WE'LL CALCULATE STARTING INDEX.

uint StartingIndex = (newPage - 1) * newPageSize;

uint oldPage = Page;

//HERE, WE'RE RAISING PREVIEW PAGE CHANGE ROUTED EVENT
RaisePreviewPageChange(Page, newPage);

Page = newPage;
ItemsSource.Clear();

ICollection<object> fetchData = PageContract.GetRecordsBy(StartingIndex, newPageSize, FilterTag);

    //FETCHING DATA FROM DATASOURCE USING PROVIDED CONTRACT
    //I'LL EXPLAIN FilterTag IN SUBSEQUENT ARTICLES
   
//RIGHT NOW IT IS NOT USED

    foreach (object row in fetchData)
    {
        ItemsSource.Add(row);
    }

    RaisePageChanged(oldPage, Page);

    //RAISING PAGE CHANGED EVENT.

}

Using control in XAML

You have to put a DataGrid/ListBox and a PaggingControl in the window. Bind its ItemsSource property to PageControl's ItemsSource property. Provide PaggingContract to the PageControl. And yes, don't forget to apply a control template to the PageControl. When you are done with these things, the PageControl is ready.

<DataGrid
ItemsSource="{Binding ItemsSource, ElementName=pageControl, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
AutoGenerateColumns="False"
CanUserAddRows="False">
  <DataGrid.Columns>
    <
DataGridTextColumn Header="First name" Binding="{Binding FirstName}" IsReadOnly="True"/>
    <DataGridTextColumn Header="Middle name" Binding="{Binding MiddleName}" IsReadOnly="True"/>
    <DataGridTextColumn Header="Last name" Binding="{Binding LastName}" IsReadOnly="True"/>
    <DataGridTextColumn Header="Age" Binding="{Binding Age}" IsReadOnly="True"/>
  </DataGrid.Columns>
</
DataGrid>

<local:PaggingControl x:Name="pageControl" Grid.Row="1" Height="25"
PageContract="{StaticResource database}"
PreviewPageChange="pageControl_PreviewPageChange"
PageChanged="pageControl_PageChanged">
  <local:PaggingControl.PageSizes>
    <
sys:UInt32>10</sys:UInt32>
    <sys:UInt32>20</sys:UInt32>
    <sys:UInt32>50</sys:UInt32>
    <sys:UInt32>100</sys:UInt32>
  </local:PaggingControl.PageSizes>
</
local:PaggingControl>

I have applied the control template using the style as below.

<Style TargetType="{x:Type local:PaggingControl}">
  <Setter Property="Template">
    <Setter.Value>
      <
ControlTemplate TargetType="{x:Type local:PaggingControl}">
        <Grid>
          <
Grid.ColumnDefinitions>
            <
ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
          </Grid.ColumnDefinitions>

          <Button Name="PART_FirstPageButton" Content="&lt;&lt;" Grid.Column="0"/>
          <Button Name="PART_PreviousPageButton" Content="&lt;" Grid.Column="1"/>
          <TextBox Name="PART_PageTextBox" Grid.Column="2"/>
          <TextBlock Text="{Binding TotalPages, RelativeSource={RelativeSource TemplatedParent}}" Grid.Column="3"/>
          <Button Name="PART_NextPageButton" Content="&gt;" Grid.Column="4"/>
          <Button Name="PART_LastPageButton" Content="&gt;&gt;" Grid.Column="5"/>
          <ComboBox Name="PART_PageSizesCombobox" Grid.Column="6"/>
        </Grid>
      </
ControlTemplate>
    </
Setter.Value>
  </
Setter>
</
Style>

Pretty simple, isn't is.!!!

I'm attaching project files. Do let me know if you have any query or suggestions. I'll cover filter functionality later on.