Click here to Skip to main content
15,880,543 members
Articles / Desktop Programming / WPF

WPF - Paging in DataGrid / ListBox

Rate me:
Please Sign up or sign in to vote.
4.85/5 (14 votes)
20 Mar 2012CPOL3 min read 90.8K   6K   26   20
Provides functionality of paging to DataGrid / ListBox control.

Problem

No paging support in DataGrid/ListBox in WPF.

Introduction

I have been working on WPF since last 3 years. Initially there was no DataGrid control in WPF. Microsoft introduced it in .NET Framework- 4. So, we were used to place ListBox as a DataGrid by applying 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 at every page for data retrieval. It was very much tedious task. So, I decided to make a generic control that can handle paging.

Overview

Rather than adding paging functionality to DataGrid, I come with another idea, making paging as a separate control. Paging-Control will take care of page retrieval task. Developer has to only bind DataGrid or ListBox to the ItemsSource provided by Paging-Control.

So, It’s kind of a plug & play system. You can plug any control which is capable of displaying ICollection<T> to PagingControl without coding. You just have to implement IpageContract interface. This interface contains only two methods, one for getting 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 subsequent article.

Implementation Details

C#
[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 TemplatePart in my paging control. It is a single .CS file inheriting Control class. Here I have used four buttons for navigation, one textbox to display the current page or set page manually, and one combobox to set page size. I used TemplatePart to give freedom to other developer to completely change UI of this control and making it simple to use.

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

C#
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 page change event, one gets fired before changing page and the other after changing the page.

C#
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 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.

C#
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 control gets loaded, we start our work.

C#
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 above code, we first check, whether control template has been applied to the PagingControl or not. After checking Template, we go for PageContract. We check if PageContract has been assigned or not. This contract is important because all data retrieval work is done by this PageContract instance.

C#
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 setup local variable properties to appropriate default values.

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

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

BindProperties will do binding of properties. Here, we have bound Page property to textbox supplied to PageControl by control template. Same for PageSizes property - ComboBox control.

C#
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 control. As we have kept SelectedIndex=0 in combobox, on finishing loading, Combobox selection gets changed. So, item change event will get fired. So, control will start loading data.

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

Above event will call a private method with navigation type. It’s an enum. It is defined as below.

C#
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 gets called from all the 6 registered events with appropriate enum value. This method contains the core logic of the paging control.

C#
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 one DataGrid/ListBox, PagingControl in the Window. Bind its ItemsSource property to PageControl’s ItemsSource property. Provide PaggingContract to the PageControl. And yes, don’t forget to apply control template to the PageControl. Once you are done with these things, PageControl is ready.

XML
<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 control template using style as below.

XML
<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="<<" Grid.Column="0"/>
                    <Button Name="PART_PreviousPageButton" Content="<" 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=">" Grid.Column="4"/>
                    <Button Name="PART_LastPageButton" Content=">>" 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. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Credit-Suisse
India India
I'm a Software Developer having 10+ Yrs of experience on Microsoft technologies.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 1169801817-May-15 17:51
Member 1169801817-May-15 17:51 
QuestionPut your codes on Tab Control, it works first time Tab Click but does not work after next Tab Click? Pin
hocviencsqg13-Nov-14 10:17
hocviencsqg13-Nov-14 10:17 
AnswerRe: Put your codes on Tab Control, it works first time Tab Click but does not work after next Tab Click? Pin
Nilay M Joshi14-Nov-14 21:13
Nilay M Joshi14-Nov-14 21:13 
QuestionConstructor overloading in MyDatabase class Pin
Zukiari27-Apr-14 21:29
Zukiari27-Apr-14 21:29 
AnswerRe: Constructor overloading in MyDatabase class Pin
Nilay M Joshi26-May-14 19:37
Nilay M Joshi26-May-14 19:37 
QuestionIs there a way to interchange the Binding . Say suppose I want to Reverse the xaml where binding is given Pin
Lucky Ahuja4-Apr-14 0:25
Lucky Ahuja4-Apr-14 0:25 
AnswerRe: Is there a way to interchange the Binding . Say suppose I want to Reverse the xaml where binding is given Pin
Nilay M Joshi16-Apr-14 1:36
Nilay M Joshi16-Apr-14 1:36 
QuestionDynamically binding Pin
selvanath22-Sep-13 22:06
selvanath22-Sep-13 22:06 
AnswerRe: Dynamically binding Pin
Nilay M Joshi16-Apr-14 1:41
Nilay M Joshi16-Apr-14 1:41 
AnswerRe: Dynamically binding Pin
Sven Bardos6-Sep-14 11:37
Sven Bardos6-Sep-14 11:37 
QuestionExample for filter Pin
farzaneh.k21-Aug-13 0:57
farzaneh.k21-Aug-13 0:57 
AnswerRe: Example for filter Pin
Nilay M Joshi26-Aug-13 8:34
Nilay M Joshi26-Aug-13 8:34 
QuestionDataGrid does not refresh Pin
NguyenAdam10-Dec-12 13:29
NguyenAdam10-Dec-12 13:29 
Dear Expert:

There 2 options of data binding to DataGrid:
1. Show All
2. Show Only Gold Customers

If Option 1 is selected, I want Datagrid bind to DataSet ds1.
If Option 2 is selected, I want Datagrid bind to DataSet ds2.

Please give me instruction (or sample code) to refresh DataGrid then bound to appropriate DataSet.

Thank you;
AnswerRe: DataGrid does not refresh Pin
Nilay M Joshi1-May-13 8:29
Nilay M Joshi1-May-13 8:29 
QuestionHow to assign PageContract to ViewModel Pin
apoorcoder12-Sep-12 22:04
apoorcoder12-Sep-12 22:04 
AnswerRe: How to assign PageContract to ViewModel Pin
apoorcoder12-Sep-12 22:11
apoorcoder12-Sep-12 22:11 
GeneralRe: How to assign PageContract to ViewModel Pin
Nilay M Joshi1-May-13 8:20
Nilay M Joshi1-May-13 8:20 
QuestionFilter, Sort Pin
R0n1n0822-Mar-12 22:22
R0n1n0822-Mar-12 22:22 
AnswerRe: Filter, Sort Pin
Nilay M Joshi23-Mar-12 2:26
Nilay M Joshi23-Mar-12 2:26 
AnswerRe: Filter, Sort Pin
Nilay M Joshi1-May-13 6:44
Nilay M Joshi1-May-13 6:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.