WPF - 在 DataGrid / ListBox 中分页






4.86/5 (14投票s)
为 DataGrid / ListBox 控件提供分页功能。
问题
WPF 中 DataGrid
/ListBox
不支持分页。
引言
我从 3 年前就开始从事 WPF 开发。最初,WPF 中没有 DataGrid
控件。微软在 .NET Framework- 4 中引入了它。因此,我们过去常常通过应用控件模板来将 ListBox
充当 DataGrid
。在我的项目中,我们有大量的数据。我们必须以块(页面)的形式检索数据。为此,我们必须在每个页面上实现 UI 逻辑 + 数据获取逻辑,这非常繁琐。所以,我决定制作一个可以处理分页的通用控件。
概述
与其向 DataGrid
添加分页功能,我提出了另一个想法,将分页作为单独的控件。分页控件将负责页面检索任务。开发人员只需要将 DataGrid
或 ListBox
绑定到分页控件提供的 ItemsSource
即可。
因此,它有点像即插即用系统。您无需编码就可以将任何能够显示 ICollection<T>
的控件插入到 PagingControl
中。您只需要实现 IpageContract
接口。此接口仅包含两个方法,一个用于获取计数,另一个用于获取数据。
在第一篇文章中,我只介绍了以块(页面)的形式获取数据,没有任何筛选或搜索条件。我将在后续文章中介绍这些内容。
实现细节
[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
{
………
}
我在分页控件中使用了 TemplatePart
。它是一个继承 Control
类的单个 .CS 文件。在这里,我使用了四个用于导航的按钮,一个文本框用于显示当前页面或手动设置页面,以及一个组合框用于设置页面大小。我使用 TemplatePart
让其他开发人员可以完全更改此控件的 UI,并使其易于使用。
我已经创建了以下依赖属性和用于绑定的相关简单属性。
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
我为页面更改事件创建了两个 RoutedEvent
,一个在更改页面之前触发,另一个在更改页面之后触发。
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
我们已经重写了 OnApplyTemplate
方法。通过这样做,我们将获取所有子控件的引用到局部变量中,以便我们可以在整个控件中引用它们。我们还确保它们都没有丢失。如果其中任何一个丢失,我们将抛出异常。
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();
}
一旦控件加载完成,我们就开始工作。
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();
}
在上面的代码中,我们首先检查控件模板是否已应用于 PagingControl
。在检查模板后,我们转向 PageContract
。我们检查是否已分配 PageContract
。这个契约很重要,因为所有数据检索工作都由这个 PageContract
实例完成。
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);
}
SetDefaultValues
方法会将局部变量属性设置为适当的默认值。
private void SetDefaultValues()
{
ItemsSource = new ObservableCollection<object>();
cmbPageSizes.IsEditable = false;
cmbPageSizes.SelectedIndex = 0;
}
BindProperties
将进行属性绑定。在这里,我们将 Page 属性绑定到控件模板为 PageControl
提供的文本框。对于 PageSizes
属性也是如此 - ComboBox
控件。
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);
}
现在,我们已经完成了设置控件。由于我们在组合框中保留了 SelectedIndex=0
,在加载完成后,组合框的选择会发生变化。因此,项目更改事件将被触发。因此,控件将开始加载数据。
void cmbPageSizes_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Navigate(PageChanges.Current);
}
上面的事件将调用一个带有导航类型的私有方法。它是一个枚举。它定义如下。
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
}
此导航方法从所有 6 个注册事件中调用,带有适当的枚举值。此方法包含分页控件的核心逻辑。
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.
}
在 XAML 中使用控件
您必须将一个 DataGrid
/ListBox
、PagingControl
放在 Window 中。将其 ItemsSource
属性绑定到 PageControl
的 ItemsSource
属性。向 PageControl
提供 PaggingContract
。是的,不要忘记将控件模板应用于 PageControl
。完成这些操作后,PageControl
就准备好了。
<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>
我使用以下方式通过样式应用了控件模板。
<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>
很简单,不是吗!!!
我附上了项目文件。如果您有任何疑问或建议,请告诉我。我稍后会介绍筛选功能。