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, Previous, Current, Next, Last }
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) {
return;
}
totalRecords = PageContract.GetTotalCount();
newPageSize = (uint)cmbPageSizes.SelectedItem;
if (totalRecords == 0)
{
ItemsSource.Clear();
TotalPages = 1;
Page = 1;
}
else
{
TotalPages = (totalRecords / newPageSize) + (uint)((totalRecords % newPageSize == 0) ? 0 : 1);
}
uint newPage = 1;
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;
}
uint StartingIndex = (newPage - 1) * newPageSize;
uint oldPage = Page;
RaisePreviewPageChange(Page, newPage);
Page = newPage;
ItemsSource.Clear();
ICollection<object> fetchData =
PageContract.GetRecordsBy(StartingIndex, newPageSize, FilterTag);
foreach (object row in fetchData)
{
ItemsSource.Add(row);
}
RaisePageChanged(oldPage, Page);
}
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.