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.
Problem
There is no paging support in DataGrid/ListBox in WPF.
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.
So, it's kind of a plug-and-play system. You can plug any control capable of displaying an 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 of Paging Control
[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 another 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 properties 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 Routed Events 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 references 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 registration 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 the 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 preceding event will call a private method with a 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)
{
return; // If no contract, return.
}
totalRecords = PageContract.GetTotalCount(); // Get new total records count.
newPageSize = (uint)cmbPageSizes.SelectedItem; // Get new page size.
if (totalRecords == 0)
{
ItemsSource.Clear(); // If no records found, clear ItemsSource.
TotalPages = 1;
Page = 1;
}
else
{
// Calculate TotalPages.
TotalPages = (totalRecords / newPageSize) + (uint)((totalRecords % newPageSize == 0) ? 0 : 1);
}
uint newPage = 1;
// Set new page variable based on change enum.
// Switch code below 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, calculate starting index.
uint StartingIndex = (newPage - 1) * newPageSize;
uint oldPage = Page;
// Here, we're raising PreviewPageChange routed event.
RaisePreviewPageChange(Page, newPage);
Page = newPage;
ItemsSource.Clear();
ICollection<object> fetchData = PageContract.GetRecordsBy(StartingIndex, newPageSize, );
// Fetching data from datasource using provided contract.
// Right now FilterTag is not used.
foreach (object row in fetchData)
{
ItemsSource.Add(row);
}
RaisePageChanged(oldPage, Page); // Raising PageChanged 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="<<" 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 it.
I'm attaching project files. Do let me know if you have any queries or suggestions. I'll cover filter functionality later on.