ASP.NET GridView控件实现高效分页






4.93/5 (49投票s)
对于大规模可扩展的应用程序,分页必须是高效的。如果没有智能高效的分页和排序功能来处理海量数据,用户请求将花费更长的时间并消耗更多资源。
引言
我们所有在ASP.NET中使用GridView
的开发者都知道,使用ASP.NET的GridView
控件实现分页是一个简单的任务。对于数以万计的数据,如果我们想开发一个可扩展的应用程序,分页是一个重要的事情。
背景
在本演示中,我将开发一个ASP.NET应用程序。首先,我将展示通常使用GridView
控件的分页。然后,我将增强分页功能,使其更有效。
开始吧
打开Microsoft SQL Server Management Studio 2005。按照以下结构设计一个表,或者您也可以使用不同的结构。
Profile
表的一些记录。

在Visual Studio 2010中创建ASP.NET网站
- 创建一个网站项目,并将其命名为
EffectivePagingAndSorting
。 - 在该项目中添加一个Web窗体,并将页面重命名为NormalPaging.aspx。
在NormalPaging.aspx页面中添加一个GridView
控件。将其重命名为gvProfile
。在NormalPaging.aspx页面中添加一个SqlDataSource
控件。将其重命名为profileDataSource
。配置SqlDataSource
控件,如下所示:
<asp:SqlDataSource ID="profileDataSource" runat="server"
ConnectionString = "<%$ ConnectionStrings:ApplicationServices %>"
SelectCommand="SELECT [ProfileId],[Name],[Address],[Email],[Mobile],
Active = CASE [IsActive] WHEN 1 THEN 'Active' WHEN 0 THEN 'DeActive'
END FROM [dbo].[Profile]" >
您可能在web.config文件中使用了不同的连接字符串。在select
命令SQL中,我使用了一个SQL CASE语句,以便从Profile
表中显示“Active”(而不是1)和“DeActive”(而不是0)。
现在配置GridView
控件以显示用户配置文件数据。将DataSourceID
设置为profileDataSource
,将AutoGenerateColumns
设置为false
(以便手动设置列),将AllowPaging
设置为true
(允许分页),将AllowSorting
设置为true
(允许通过单击列标题进行排序),将PageSize
设置为5
(以便grid
一次只显示5条记录)。以及其他style
属性,如下面的标记所示,或者根据您的需要进行设置。
<asp:GridView ID="gvProfile" DataSourceID="profileDataSource" runat="server"
AutoGenerateColumns="false" AllowPaging="true" AllowSorting="true"
PageSize="5" HeaderStyle-Font-Names="Verdana" Font-Size="Small"
HeaderStyle-HorizontalAlign="Left"
HeaderStyle-Font-Underline="false" Width="55%"
HeaderStyle-BackColor="BurlyWood" HeaderStyle-ForeColor="Navy">
<alternatingrowstyle backcolor="</span>Aquamarine"> />
<columns>
<asp:BoundField DataField="ProfileId" HeaderText="Profile Id"
SortExpression="ProfileId" ItemStyle-Width="6%"/>
<asp:BoundField DataField="Name" HeaderText="Name"
SortExpression="Name" ItemStyle-Width="13%"/>
<asp:BoundField DataField="Address" HeaderText="Address"
SortExpression="Address" ItemStyle-Width="18%"/>
<asp:BoundField DataField="Email" HeaderText="Email"
SortExpression="Email" ItemStyle-Width="8%"/>
<asp:BoundField DataField="Mobile" HeaderText="Mobile"
SortExpression="Mobile" ItemStyle-Width="9%"/>
<asp:BoundField DataField="IsActive" HeaderText="Status"
SortExpression="IsActive" ItemStyle-Width="4%"/>
在上面的标记中,网格列已手动绑定到数据源。
运行应用程序浏览NormalPaging.aspx。您将在Web浏览器中看到如下所示的结果:

工作原理
为了在NormalPaging.aspx页面上使用GridView
显示这些记录,我为SqldataSource
编写了select
命令,然后将SqlDataSource
指定为GridView
控件的数据源。实际上,对于NormalPaging.aspx页面的每一次加载,都会从底层Profile
表中检索所有记录到内存中。然后Gridview
按页显示这些记录。设想一下,如果底层表有数百万条记录会发生什么。您肯定会同意,当页面加载时,它会检索所有数百万条记录。因此,加载时间会很长。并且加载完成后,它必须占用大量的系统资源。因此,这种分页方式会降低应用程序的可扩展性。所以,对于成千上万条记录,这种分页方式并不有效。
高效分页
为了使分页和排序有效,第一步是在SQL中创建存储过程。
- 打开 SQL Server Management Studio
- 新建查询
在您的数据库中创建一个具有某种技术的存储过程,以从Profile
表中检索记录。例如,如下SQL:
CREATE PROCEDURE [dbo].[Profile_GET]
@PageSize int = null,
@CurrentPage int = null,
@SortExpression nvarchar(max) = null
AS
BEGIN
SET NOCOUNT ON
DECLARE @SqlString nvarchar(max)
Declare @UpperBand int
Declare @LowerBand int
SET @LowerBand = (@CurrentPage - 1) * @PageSize
SET @UpperBand = (@CurrentPage * @PageSize) + 1
BEGIN
SET @SqlString='WITH tempProfile AS
(
SELECT
[ProfileId],
[Name],
[Address],
[Email],
[Mobile],
[Active] = CASE [IsActive] WHEN 1 _
THEN ''Active'' WHEN 0 THEN ''DeActive'' END,
ROW_NUMBER() OVER (ORDER BY '+ _
@SortExpression +' ) AS RowNumber
FROM [dbo].[Profile]
)
SELECT
[ProfileId],
[Name],
[Address],
[Email],
[Mobile],
[Active]
FROM
tempProfile
WHERE
RowNumber > ' + CONVERT(VARCHAR,@LowerBand) + _
' AND RowNumber < ' + CONVERT(VARCHAR, @UpperBand)
+ ' ORDER BY ' + @SortExpression
EXEC sp_executesql @SqlString
END
END
Profile_GET
存储过程接受PageSize
、CurrentPage
和SortExpression
作为输入参数。PageSize
- 是GridView
一次显示一页的记录数。CurrentPage
– 您当前在GridView
中的页面。SortExpression
– 按哪个字段对一页的记录进行排序。
所有这些参数都将在应用程序运行时由GridView
控件从Web页面传递。
在过程中,设置SET NOCOUNT ON
,这样查询就不会产生像下面这样的消息,这些消息会增加查询的输出量。
(29 row(s) affected)
SET @LowerBand = (@CurrentPage - 1) * @PageSize
SET @UpperBand = (@CurrentPage * @PageSize) + 1
以上两个方程将计算一页的上限和下限。下限是指一页的起始行位置,上限是指一页的顶部行位置。假设您当前在第5页,页面大小为5。那么以上两个方程的结果将是:
LowerBand = (5 -1) * 5 = 20
UpperBand = (5 * 5) + 1 = 26
因此,这些方程将返回位置编号为21-25的记录(因为我在后面的过程中在where
条件中使用了大于和小于)。
WITH tempProfile AS
(
SELECT
[ProfileId],
[Name],
[Address],
[Email],
[Mobile],
[Active] = CASE [IsActive] WHEN 1 THEN 'Active' _
WHEN 0 THEN 'DeActive' END,
ROW_NUMBER() OVER (ORDER BY '+ _
@SortExpression +' ) AS RowNumber
FROM [dbo].[Profile]
)
WITH SQL语句后跟表名,将从其内部的SELECT
SQL语句生成一个临时表。
ROW_NUMBER() OVER (ORDER BY '+ @SortExpression +' ) AS RowNumber
上面的SQL语句将向临时表tempProfile
添加一个名为RowNumber
的附加列,并在对临时表中的每个记录按排序表达式参数升序排序后为其分配一个顺序编号。这个RowNumber
列将用于后续的分页。

临时tempProfile
表中的一些记录,其中记录已按Profile Id排序。
现在根据PageSize
、CurrentPage
和SortExpresssion
参数的请求过滤记录。
SELECT
[ProfileId],
[Name],
[Address],
[Email],
[Mobile],
[Active]
FROM
tempProfile
WHERE
RowNumber > ' + CONVERT(VARCHAR,@LowerBand) + ' _
AND RowNumber < ' + CONVERT(VARCHAR, @UpperBand)
+ ' ORDER BY ' + @SortExpression
我通过EXEC sp_executesql
执行了动态SQL,因为SortExpression
是通过参数传递的。
EXEC sp_executesql @SqlString
现在,您需要创建另一个存储过程。
CREATE PROCEDURE [dbo].[Profile_Total]
AS
BEGIN
SET NOCOUNT ON
SELECT COUNT(*) FROM Profile
END
上面的Profile_Total
将返回记录的总数。
现在回到Visual Studio。添加一个类,我创建了ProfileDataSource.cs。此类型将由对象数据源使用。因此,我们需要使其成为对象数据源的数据源。
[DataObject(true)]
public class ProfileDataSource
{
public ProfileDataSource()
{
}
[DataObjectMethod(DataObjectMethodType.Select, false)]
public Int32 TotalRowCount(Int32 startRowIndex,
Int32 pageSize, String sortExpression)
{
Int32 intTotalProfile = 0;
using (SqlConnection conn = new SqlConnection
(ConfigurationManager.ConnectionStrings["ApplicationServices"].ToString()))
{
SqlCommand cmdSelect = new SqlCommand();
conn.Open();
cmdSelect.CommandText = "Profile_Total";
cmdSelect.CommandType = CommandType.StoredProcedure;
cmdSelect.Connection = conn;
SqlDataReader dataReader = cmdSelect.ExecuteReader();
dataReader.Read();
intTotalProfile = Convert.ToInt32(dataReader[0]);
}
return intTotalProfile;
}
[DataObjectMethod(DataObjectMethodType.Select, true)]
public static DataTable GetProfileData
(Int32 startRowIndex, Int32 pageSize, String sortExpression)
{
DataTable profileDataTable = new DataTable();
using (SqlConnection conn = new SqlConnection
(ConfigurationManager.ConnectionStrings["ApplicationServices"].ToString()))
{
SqlCommand cmdSelect = new SqlCommand();
conn.Open();
cmdSelect.CommandText = "Profile_GET";
cmdSelect.CommandType = CommandType.StoredProcedure;
cmdSelect.Connection = conn;
startRowIndex = Convert.ToInt32(startRowIndex / pageSize) + 1;
if (String.IsNullOrEmpty(sortExpression))
sortExpression = "ProfileId";
cmdSelect.Parameters.AddWithValue("@CurrentPage", startRowIndex);
cmdSelect.Parameters.AddWithValue("@PageSize", pageSize);
cmdSelect.Parameters.AddWithValue("@SortExpression", sortExpression);
SqlDataAdapter dataAdapter = new SqlDataAdapter();
dataAdapter.SelectCommand = cmdSelect;
dataAdapter.Fill(profileDataTable);
}
return profileDataTable;
}
}
添加以下命名空间
using System.Configuration;
using System.Data.SqlClient;
using System.ComponentModel;
现在通过添加DataObject
属性将ProfileDataSource
类型标记为数据对象。
[DataObject(true)]
public class ProfileDataSource
{
}
在该类型中添加两个方法。TotalRowCount
用于获取底层表中的总记录数。GetProfileData
用于从底层表中获取分页记录。
现在为数据对象Select
方法编写方法。
[DataObjectMethod(DataObjectMethodType.Select, false)]
public Int32 TotalRowCount(Int32 startRowIndex, Int32 pageSize, String sortExpression)
{
}
[DataObjectMethod(DataObjectMethodType.Select, true)]
public static DataTable GetProfileData(Int32 startRowIndex,
Int32 pageSize, String sortExpression)
{
}
为TotalRowCount
方法编写正文。此方法的参数稍后将由对象数据源控件传递。它将执行我们已经创建的Ptofile_Total
存储过程,并将总记录数返回给对象数据源控件。
[DataObjectMethod(DataObjectMethodType.Select, false)]
public Int32 TotalRowCount(Int32 startRowIndex, Int32 pageSize, String sortExpression)
{
Int32 intTotalProfile = 0;
using (SqlConnection conn = new SqlConnection
(ConfigurationManager.ConnectionStrings["ApplicationServices"].ToString()))
{
SqlCommand cmdSelect = new SqlCommand();
conn.Open();
cmdSelect.CommandText = "Profile_Total";
cmdSelect.CommandType = CommandType.StoredProcedure;
cmdSelect.Connection = conn;
SqlDataReader dataReader = cmdSelect.ExecuteReader();
dataReader.Read();
intTotalProfile = Convert.ToInt32(dataReader[0]);
}
return intTotalProfile;
}
为GetProfileData
方法编写正文。此方法的参数将由对象数据源控件传递。它将执行我们已经创建的Profile_GET
存储过程,并带参数以在每次请求时获取分页记录。最后,此方法将返回一个数据表给对象数据源控件。
[DataObjectMethod(DataObjectMethodType.Select, true)]
public static DataTable GetProfileData(Int32 startRowIndex,
Int32 pageSize, String sortExpression)
{
DataTable profileDataTable = new DataTable();
using (SqlConnection conn = new SqlConnection
(ConfigurationManager.ConnectionStrings["ApplicationServices"].ToString()))
{
SqlCommand cmdSelect = new SqlCommand();
conn.Open();
cmdSelect.CommandText = "Profile_GET";
cmdSelect.CommandType = CommandType.StoredProcedure;
cmdSelect.Connection = conn;
startRowIndex = Convert.ToInt32(startRowIndex / pageSize) + 1;
if (String.IsNullOrEmpty(sortExpression))
sortExpression = "ProfileId";
cmdSelect.Parameters.AddWithValue("@CurrentPage", startRowIndex);
cmdSelect.Parameters.AddWithValue("@PageSize", pageSize);
cmdSelect.Parameters.AddWithValue("@SortExpression", sortExpression);
SqlDataAdapter dataAdapter = new SqlDataAdapter();
dataAdapter.SelectCommand = cmdSelect;
dataAdapter.Fill(profileDataTable);
}
return profileDataTable;
}
startRowIndex = Convert.ToInt32(startRowIndex / pageSize) + 1;
这行代码根据startRowIndex
和pageSize
参数计算开始行索引。
当页面首次加载时,对象数据源控件会将sortExpression
参数传递为null
。在这种情况下,sort
表达式将是ProfileId
,这是底层表的主键。
if (String.IsNullOrEmpty(sortExpression))
sortExpression = "ProfileId";
EffectivePaging.aspx
将NormalPaging.aspx中的GridView
标记复制到EffectivePaging.aspx。
<asp:GridView ID="gvProfile" DataSourceID="profileDataSource" runat="server"
AutoGenerateColumns="false" AllowPaging="true" AllowSorting="true" PageSize="5"
HeaderStyle-Font-Names="Verdana" Font-Size="Small"
HeaderStyle-HorizontalAlign="Left" HeaderStyle-Font-Underline="false"
Width="55%" HeaderStyle-BackColor="BurlyWood"
HeaderStyle-ForeColor="Navy">
<alternatingrowstyle backcolor="</span>Aquamarine"> />
<columns>
<asp:BoundField DataField="ProfileId" HeaderText="Profile Id"
SortExpression="ProfileId" ItemStyle-Width="6%"/>
<asp:BoundField DataField="Name" HeaderText="Name"
SortExpression="Name" ItemStyle-Width="13%"/>
<asp:BoundField DataField="Address" HeaderText="Address"
SortExpression="Address" ItemStyle-Width="18%"/>
<asp:BoundField DataField="Email" HeaderText="Email"
SortExpression="Email" ItemStyle-Width="8%"/>
<asp:BoundField DataField="Mobile" HeaderText="Mobile"
SortExpression="Mobile" ItemStyle-Width="9%"/>
<asp:BoundField DataField="IsActive" HeaderText="Status"
SortExpression="IsActive" ItemStyle-Width="4%"/>
在Effectivepaging.aspx页面中添加一个对象数据源控件。我已经创建了profileDataSource
。将其设置为gvProfile
数据源ID。
DataSourceID="profileDataSource"
<asp:ObjectDataSource ID="profileDataSource" runat="server"
SelectMethod="GetProfileData" EnablePaging="true"
MaximumRowsParameterName="pageSize"
StartRowIndexParameterName="startRowIndex"
TypeName="VTS.Web.UI.ProfileDataSource" SelectCountMethod="TotalRowCount"
SortParameterName="sortExpression">
<SelectParameters>
<asp:Parameter Name="startRowIndex" Type="Int32" />
<asp:Parameter Name="pageSize" Type="Int32"/>
<asp:Parameter Name="sortExpression" Type="String" />
</SelectParameters>
设置一些属性:将EnablePaging
设置为true
以启用分页,
将MaximumRowsParameterName
设置为pageSize
以指定当前页面大小,
将StartRowIndexParameterName
设置为startRowIndex
以指定开始行号位置,
将TypeName
设置为VTS.Web.UI.ProfileDataSource
(用于具有命名空间的Data
对象),
将SelectMethod
设置为GetProfileData
以获取分页记录,
将SelectCountMethod
设置为TotalRowCount
以获取底层表的总记录数,这将用于分页,
将SortParameterName
设置为sortExpression
以进行排序。
对象数据源的Select
参数
<SelectParameters>
<asp:Parameter Name="startRowIndex" Type="Int32" />
<asp:Parameter Name="pageSize" Type="Int32"/>
<asp:Parameter Name="sortExpression" Type="String" />
</SelectParameters>
现在运行应用程序以在浏览器中浏览EffectivePaging.aspx页面。您将看到如下所示的结果:

工作原理
在SQL Management Studio的“工具”菜单下打开SQL Profile,以诊断高效分页的幕后工作原理。- 从“文件”菜单打开新跟踪
- 使用您的凭据登录
- 选择“运行”
- 在“编辑”菜单下单击“清除跟踪窗口”以清除现有跟踪。
- 最小化SQL Server Profiler
- 从Visual Studio运行应用程序以浏览EffectivePaging.aspx页面
首次运行应用程序后,您将获得如下所示的跟踪:

从Profiler复制跟踪,并在SQL Management Studio中使用SQL数据库运行它。
exec Profile_GET @CurrentPage=1,@PageSize=5,@SortExpression=N'ProfileId'
您将获得以下输出:

现在清除SQL Profiler中已创建的跟踪并将其最小化。
单击当前正在运行的EffectivePaging.aspx页面上的第2页。返回到SQL Profiler,获取第2页的跟踪。
exec Profile_GET @CurrentPage=2,@PageSize=5,@SortExpression=N'ProfileId'
在SQL Management Studio中使用SQL数据库运行它。您将得到如下结果:

对GridView
底部的所有可用页面继续执行这些步骤,还可以通过单击每个列的标题来排序。每次请求都会获得5条记录。因此,不会出现加载底层表中所有记录的情况。只加载您在GridVew
的pageSize
属性中设置的记录数。记录加载速度比以前的普通分页快。它将占用更少的系统资源。对于成千上万条数据来说,这是非常有效的。
结论
对于处理大量记录以构建可扩展应用程序的应用程序来说,高效分页非常重要。本演示将帮助您创建可扩展的应用程序。