Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF - Paging in DataGrid / ListBox

0.00/5 (No votes)
20 Mar 2012 1  
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

[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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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

<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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here