ASP.NET 3.5中新增加的ListView控件是一个用于页面数据绑定和界面布局的非常棒的控件,它在ASP.NET 2.0 GridView的基础上做了很多改进,用户在使用时可以控制的元素更多,开发时的灵活性更大了,个人觉得ListView在使用时比DataGrid和GirdView更加顺手。ListView控件本身并没有分页功能,不过借助于ASP.NET中新增加的DataPager控件,我们可以非常方便地对ListView中的数据设置分页,这几乎不需要开发人员写一行代码,将ListView控件放到页面上,设置好布局和DataSource,然后再添加一个DataPager控件,将它的PagedControlID属性设置成ListView的ID,PageSize中设置每页要显示的数据条数,然后在Fields中设置好分页的样式(当然你完全可以不用去管样式,ASP.NET会根据内置的样式来定义分页UI),运行Web程序,你就会看到一个支持分页的ListView页面,整个过程非常简单。在ASP.NET 3.5中,微软将数据绑定控件和分页控件分离开来了,这样用户就可以在页面的任何地方设置分页,并根据自己的需要定义各种不同的分页样式,同时,分页控件可以控制任意一个数据绑定控件,它们之间通过PagedControlID属性来指定,基本上对于分页操作,开发人员可以随心所欲了。如何使用ListView控件和DataPager控件不是本文的重点,感兴趣的读者可以去查一下微软的MSDN,我想它应该比我讲得要详细得多。
先说说数据分页的原理。我们在开发数据绑定页面的Web应用程序时,常常会遇到数据量比较多的情况,为了防止页面变得过大加载数据慢的问题,大家都会将一个页面上要显示的数据通过分页来完成,用户访问页面时通过分页功能来查看不同页面中的数据,这是一个非常好的解决办法,而且几乎所有的程序设计人员和开发人员都会不约而同乐此不彼地采用分页的方式来显示页面上的数据,这没有什么问题! 问题在于分页的方式。
一般情况下,最简单的实现方法是一次性将所有页面的数据读到缓存媒介中(这个媒介一般都是服务器的内存),然后每次只显示一页的数据。这种方式实现起来很容易,而且ASP.NET之前几乎所有的支持分页的数据绑定控件都是采用的这种方式,以至于很多ASP.NET的初学者都采用了这样的方式来开发分页数据绑定页面,并且没有觉察出任何问题。是的,程序开发中采用最简单有效的方法一般都是不会出现什么问题的,况且微软提供的标准控件都是这样做的,会有什么问题呢?对于一些小的Web应用程序而言,这确实没有什么问题,因为它涉及到的数据量比较小,即使我们将所有的数据都读到内存中,充其量也才几兆,多一点十几兆,几十兆。如果这些数据都是纯文本的话(一般而言我们保存在数据库中的数据都是文本信息),几十兆的数据已经是成千上万条记录了,现在的服务器硬件条件都比较好,内存都在G级以上,处理这点数据根本不在话下。但是,如果数据库中的一个表的记录达到上亿条,并且有些字段存储的是文件数据(也就是二进制数据),这样一次性将所有的数据读到内存中就不是一个理想的做法了,这个时候就需要采用“真分页”方式读取数据。
大部分情况下,我们还是需要采用“真分页”的方式来获取数据的。给定每页记录的起始位置(或者页面的索引),再给定一个每页显示的数据的条数和总记录数,我们希望每次取到的只是当前页面的数据。每次当用户分页时,根据这些条件从数据库中取一部分数据绑定到页面上,这样可以大大减少服务器的开销,并且再大的数据量也不是问题。这种方式似乎是理想的,然而结合用户的需求,我们会发觉即使采用“真分页”方式对数据进行分页获取,也还是会遇到问题。试想,在当今Ajax横行的Web世界里,利用Ajax方式改善用户体验的站点层出不穷,如果你恰好有一个采用Ajax方式提供的分页数据绑定页面,那问题就会出现了。由于Ajax的用户体验效果是页面的局部刷新,在分页数据绑定页面中,用户点击分页按钮后页面会以较快的速度更新分页后的数据,这个体验对用户来说是相当不错的,但是贪婪的用户有可能会想试试频繁地点击分页按钮,甚至于疯狂的用户狂点分页按钮,这个时候你的应用程序由于需要非常频繁地去数据库中获取分页数据而来不及更新页面上的数据而出现脚本错误,最终给用户的体验就是页面的分页功能不正常,程序崩溃了。
采用“真分页”和“假分页”相结合的方式可以很有效得解决上面提到的这个问题。我将上面提到的第一种数据分页方式称之为“假分页”,而将第二种数据分页方式称之为“真分页”。这两种分页方式的结合,就是说一次性读取n页的数据到缓存中,分页时根据需要判断是否从缓存中直接获取数据还是重新从数据库中加载数据到缓存里。毕竟,从缓存中加载数据效率要高得多。这样,每次用户点击分页按钮时,只要数据存在于缓存里,就可以以非常快的速度加载数据,如果缓存过期或者用户要获取的数据超出了缓存,就从数据库中重新加载新的n页数据到缓存中。当然,更新缓存的过程你可以在Ajax中采用同步采用,以限制用户在这个过程中的UI操作。
其实,分页中所涉及到的细节问题是很多的,要想详细叙述并讲清楚这其中的所有问题,光靠本文的只言片语恐怕是远远不够的,这里我只想向大家介绍一种在ASP.NET Ajax方式下进行真分页编程的一种方法。为了比较简单地使用Ajax方式,我在Visual Studio中直接使用了微软提供的ajaxToolkit包里的Ajax控件,这些控件一般来说都还是挺好用的,这里不对这些控件的使用做介绍了。
在写这篇文章之前我也查阅了很多资料,其实大家在开发数据绑定页面时一般都会采用“真分页”的方式来对数据进行分页处理,ASP.NET 3.5中的DataPager控件是一个用于数据分页的不错的控件,有的人把微软提供的数据绑定控件不支持数据“真分页”的缺陷归到它的头上,我认为这是对它的冤枉。DataPager只负责分页操作,它不管数据源的事情,它更重要的工作在于如何处理分页UI以及与用户的交互。那么,数据源怎么处理呢?数据绑定控件如何知道我的数据源被分成了多少页,我当前取的是哪一页的数据呢?
这些问题也一度让我很苦恼,我尝试过使用.NET中的PagedDataSource对象对数据进行分页,但是后来发觉这个对象也是需要一次性将所有的数据读到内存中才支持分页的,说白了,它也是一个“假分页”数据源对象,和DataGrid、GridView没有什么不同。记得从.NET 2.0开始,微软提供了一系列数据源控件(诸如SqlDataSource、XmlDataSource、LinqDataSource等等)来简化对数据绑定控件的数据源指定,其实我觉得这些控件除了简化代码外没有什么大的价值,有的时候还会破坏程序本身的结构,我一向都反对在页面上直接使用这些控件(当然,做一些演示用的程序使用这些控件还是非常便捷的)。不过我在研究Ajax真分页的过程中无意间看到了Visual Studio工具箱中的ObjectDataSource这个控件,起初我只是认为它应该是那些DataSource控件的基控件,后来通过查资料才知道,这个控件是所有的DataSource控件中唯一支持“真分页”操作的控件,它可以通过设定几个简单的属性就达到数据分页的功能,下面我就向大家介绍一下如何使用这个控件。
这个控件的使用很简单,我们只需要配置几个属性就可以了。
SelectMethod:指定用于获取分页数据的方法名。这个方法是一个自定义的.NET方法,你可以写在页面的CodeBehind代码中,将方法的名字给ObjectDataSource控件的SelectMethod即可。ObjectDataSource控件会通过委托的方式自动去执行你所指定的这个方法。
TypeName:使用ObjectDataSource控件的类的全名称(包括名称空间和类名)。这个属性必须指定,ASP.NET会通过反射来加载相应的方法和对象。
DataObjectTypeName:数据源对象的类型全名称。ObjectDataSource控件最大的亮点就在于它完全支持面向对象数据操作。在分层应用程序开发中,数据访问层的代码会将数据库中的表抽象为class对象,将数据库表中的字段抽象为class对象中的属性,DataObjectTypeName属性所指定的就是这个数据库类对象。
EnablePaging:如果你想开启数据分页功能,就需要将这个属性的值设置为True。
MaximumRowsParameterName:这个属性的值是一个参数名(只是一个参数名,而不是每页显示的数据条数),用于指示每页要显示数据的条数,ObjectDataSource控件根据委托在之前SelectMethod属性所指定的方法中传递该参数并执行其中的代码。注意,这个参数的名称必须与SelectMethod属性所指定的方法中的参数名称完全一样。
StartRowIndexParameterName:这个属性也是一个参数名,用于指示每页起始记录的索引。用法与MaximumRowsParameterName相同。
SelectCountMethod:这个属性是一个方法的签名,用来告诉ObjectDataSource控件通过什么方式得知数据源中总记录的条数。ObjectDataSource控件同样通过委托来执行这个方法,所以方法的签名必须与属性的值完全一样。
然后我们在页面上放置一个ListView控件(或者其它任何一个数据绑定控件),将它的DataSourceID属性的值设置为ObjectDataSource的ID,然后添加一个DataPager控件,将PagedControlID属性的值设置为ListView的ID。
这是我所取的数据源中的三张数据表的结构关系图,其中主表是Shoutout表,Shoutout中的一条记录对应着多个Image,它们通过BaseComment表进行关联。在下面我会给出如果获取Shoutout分页数据的存储过程的代码。
<%
@ Page Language
=
"
C#
"
AutoEventWireup
=
"
true
"
CodeBehind
=
"
AllShoutout.aspx.cs
"
Inherits
=
"
ShoutoutWallTest.AllShoutout
"
%>
<%
@ Register Assembly
=
"
AjaxControlToolkit, Version=3.0.20820.415, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e
"
Namespace
=
"
AjaxControlToolkit
"
TagPrefix
=
"
ajaxToolkit
"
%>
<!
DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<
html
xmlns
="http://www.w3.org/1999/xhtml"
>
<
head
runat
="server"
>
<
title
>
Untitled Page
</
title
>
<
link
href
="Css/style.css"
rel
="stylesheet"
type
="text/css"
/>
</
head
>
<
body
>
<
form
id
="form1"
runat
="server"
>
<
asp:ScriptManager
ID
="ScriptManager1"
runat
="server"
>
</
asp:ScriptManager
>
<
div
id
="shoutoutall"
>
<
div
style
="float: left;"
>
<
asp:UpdatePanel
ID
="UpdatePanel1"
runat
="server"
UpdateMode
="Conditional"
>
<
ContentTemplate
>
<
asp:ObjectDataSource
ID
="ObjectDataSource1"
runat
="server"
SelectMethod
="LoadShoutouts"
TypeName
="ShoutoutWallTest.AllShoutout"
DataObjectTypeName
="Model.Shoutout"
EnablePaging
="True"
MaximumRowsParameterName
="maxRows"
StartRowIndexParameterName
="startIndex"
SelectCountMethod
="CountAll"
></
asp:ObjectDataSource
>
<
div
id
="shoutoutalldescription"
>
<
asp:ListView
ID
="lvShoutout"
DataSourceID
="ObjectDataSource1"
runat
="server"
ItemPlaceholderID
="layoutTableTemplate"
DataKeyNames
="ID"
OnItemDataBound
="lvShoutout_ItemDataBound"
>
<
LayoutTemplate
>
<
div
id
="layoutTableTemplate"
runat
="server"
>
</
div
>
</
LayoutTemplate
>
<
ItemTemplate
>
<
div
class
="shoutoutallcontent"
>
<
div
class
="shoutoutalltext"
>
<%
#
Eval
(
"
Description
"
)
%>
</
div
>
<
div
>
<!--
Add images here
-->
<
asp:ListView
ID
="lvImages"
runat
="server"
ItemPlaceholderID
="layoutImages"
DataSource
='<%#Eval("Images")%
>
'>
<
LayoutTemplate
>
<
div
id
="layoutImages"
runat
="server"
>
</
div
>
</
LayoutTemplate
>
<
ItemTemplate
>
<
a
href
='Thumbnail.aspx?isthumbnail=false&basecommentid=<%#Eval("BaseCommentID").ToString()
%
>
&imagetitle
=
<%
#
Eval
(
"
Title
"
)
%>
'
target="_blank">
<
img
src
='Thumbnail.aspx?basecommentid=<%#Eval("BaseCommentID").ToString()
%
>
&imagetitle
=
<%
#
Eval
(
"
Title
"
)
%>
'
alt="" class="shoutoutimg" />
</
a
>
</
ItemTemplate
>
</
asp:ListView
>
</
div
>
<
div
class
="shoutoutallposted"
>
Posted by:
<%
#
Eval
(
"
PostedByName
"
)
%>
<%
#((DateTime)
Eval
(
"
PostedDate
"
)).ToShortDateString()
%>
</
div
>
<
div
class
="shoutoutallfooter"
>
<
asp:Button
ID
="btEdit"
CssClass
="shoutoutalllistbutton"
OnClick
="btEdit_Click"
runat
="server"
Text
="Edit"
/>
<
asp:Button
ID
="btDel"
CssClass
="shoutoutalllistbutton"
OnClick
="btDel_Click"
runat
="server"
Text
="Delete"
OnClientClick
="return confirm('Are you sure delete it?');"
/>
</
div
>
</
div
>
</
ItemTemplate
>
</
asp:ListView
>
</
div
>
<
div
class
="shoutoutallfooter"
>
<
asp:DataPager
ID
="DataPager1"
runat
="server"
PagedControlID
="lvShoutout"
PageSize
="25"
>
<
Fields
>
<
asp:NextPreviousPagerField
ButtonType
="Image"
FirstPageText
="Go to first page"
FirstPageImageUrl
="./Images/ShoutOut_ViewAll_Left.gif"
ShowFirstPageButton
="true"
ShowNextPageButton
="false"
ShowPreviousPageButton
="false"
/>
<
asp:NumericPagerField
NumericButtonCssClass
="shoutoutallnumericpager"
ButtonType
="Button"
PreviousPageImageUrl
="./Images/ShoutOut_ViewAll_Left.gif"
NextPreviousButtonCssClass
="shoutoutallnextprepager"
NextPageText
=">>"
PreviousPageText
="<<"
CurrentPageLabelCssClass
="shoutoutallcurrentpager"
ButtonCount
="5"
/>
<
asp:NextPreviousPagerField
ButtonType
="Image"
LastPageText
="Go to last page"
LastPageImageUrl
="./Images/ShoutOut_ViewAll_Right.gif"
ShowLastPageButton
="true"
ShowNextPageButton
="false"
ShowPreviousPageButton
="false"
/>
</
Fields
>
</
asp:DataPager
>
</
div
>
</
ContentTemplate
>
</
asp:UpdatePanel
>
</
div
>
</
div
>
</
form
>
</
body
>
</
html
>
我把数据绑定控件和分页控件都放在UploadPanel控件中,这样页面就会在不刷新的情况下执行数据绑定和分页操作。代码中使用了一个嵌套的ListView,原因是一条Shoutout记录会对应多条image记录,结合数据层的数据实体类,Shoutout class中会有一个类似于List<Image> Images的属性,所以我直接将这个属性作为了子ListView控件的数据源,它主要用于显示每条Shoutout记录中的缩略图。至于如何在页面中显示缩略图不是本文的重点,这里不做介绍了。代码中我们已经给ObjectDataSource控件指定了用于进行数据分页的参数名或方法签名,下面我们需要实现这些方法。
只有两个方法,LoadShoutouts()方法用于获取数据对象Shoutout的集合,也就是List<Shoutout>类型的返回值,事实上,该方法只需要执行数据库中用于分页的存储过程即可,这个存储过程可以同时返回数据集合和总记录条数。下面我会给出这个存储过程。CountAll()方法仅仅只返回总的记录条数。
using
System;
using
System.Collections;
using
System.Configuration;
using
System.Data;
using
System.Linq;
using
System.Web;
using
System.Web.Security;
using
System.Web.UI;
using
System.Web.UI.HtmlControls;
using
System.Web.UI.WebControls;
using
System.Web.UI.WebControls.WebParts;
using
System.Xml.Linq;
using
System.Collections.Generic;
using
Model;
using
BLL;
namespace
ShoutoutWallTest
{
public
partial
class
AllShoutout : System.Web.UI.Page
{
public
static
List
<
Shoutout
>
list
=
null
;
private
static
int
ItemCount
=
0
;
protected
void
Page_Load(
object
sender, EventArgs e)
{
}
public
List
<
Shoutout
>
LoadShoutouts(
int
startIndex,
int
maxRows)
{
int
itemCount;
int
pageIndex
=
1
;
if
(startIndex
>
0
)
{
pageIndex
=
(startIndex)
/
25
+
1
;
}
list
=
ShoutoutBLL.GetShoutouts(
13
, pageIndex, maxRows,
true
,
out
itemCount);
ItemCount
=
itemCount;
return
list;
}
public
int
CountAll()
{
return
ItemCount;
}
///
<summary>
///
Refresh data after updating and deleting.
///
</summary>
private
void
RefreshData()
{
lvShoutout.DataSourceID
=
ObjectDataSource1.ID;
}
}
}
我的代码中要求每页显示25条数据,ShoutoutBLL.GetShoutouts()方法有5个参数,第一个参数用于指定检索数据的条件,这个是程序中的特例,读者可以不用关心;第二个参数是页面的索引,规定从1开始,我在方法中从startIndex转换成了pageIndex;第三个参数是每页显示的数据条数;第四个参数是out类型的,返回记录总行数,这个方法主要是为了对应执行数据库的存储过程,具体代码在BLL命名空间中,属于业务逻辑层的代码,这里就不再具体给出了,Model命名空间中的代码主要用来返回数据库实体对象,如Shoutout和Image对象。RefreshData()方法中重新给ListView控件的DataSourceID属性指定了值,这样可以重新绑定数据从而达到刷新数据的效果。
上图是程序运行后的部分截图,可以看出分页UI已经显示出来了,而且对于分页操作,我没有写一行代码,这个完全由DataPager自己来控制。由于ListView和DataPager控件都位于UpdatePanel控件中,当用户点击分页按钮时页面只是更新了ListView中的数据而没有刷新整个页面,并且数据是逐页从数据库中得到的,这样便实现了在Ajax方式下的“真分页”操作。核心控件是ObjectDataSource。下面是我用于获得分页数据的存储过程,读者可以借鉴一下,这个存储过程采用了临时表的方式进行数据分页。
set
ANSI_NULLS
ON
set
QUOTED_IDENTIFIER
ON
go
ALTER
PROCEDURE
[
dbo
]
.
[
GetShoutOuts
]
--
Add the parameters for the stored procedure here
(
@LocationID
INT
,
@PageIndex
INT
,
--
start from 1.
@PageSize
INT
,
@showimages
BIT
,
@ItemCount
INT
Output
)
AS
BEGIN
--
SET NOCOUNT ON added to prevent extra result sets from
--
interfering with SELECT statements.
SET
NOCOUNT
ON
;
declare
@beginRowNumber
bigint
,
@endRowNumber
bigint
set
@beginRowNumber
=
(
@PageIndex
-
1
)
*
@PageSize
+
1
;
set
@endRowNumber
=
@PageIndex
*
@PageSize
;
WITH
TempPagingRecord
AS
(
SELECT
ROW_NUMBER()
OVER
(
ORDER
BY
PostedDate
DESC
)
AS
RecordNumber,SO.ID
AS
ShoutOutID, BC.
[
Description
]
, BC.PostedByName, BC.PostedDate,
null
as
ImageTitle,
null
as
ImageBlob,
null
as
Type,BC.ID
AS
BaseCommentID,BC.DisplayUserName,
BC.IsVisible, SO.NotifyToShoutOutUser, SO.ShoutOutToUserAlias
FROM
dbo.ShoutOut
as
SO
JOIN
dbo.BaseComment
as
BC
ON
BC.ID
=
SO.BaseCommentID
WHERE
SO.LocationID
=
@LocationID
AND
BC.IsVisible
=
1
)
SELECT
RecordNumber,
ShoutOutID,
Description,
PostedByName,
PostedDate,
ImageTitle,
ImageBlob,
BaseCommentID,
DisplayUserName,
IsVisible,
NotifyToShoutOutUser,
ShoutOutToUserAlias
INTO
#tempTable
FROM
TempPagingRecord
Where
RecordNumber
between
@beginRowNumber
and
@endRowNumber
--
Insert statements for procedure here
IF
(
@showimages
=
1
)
begin
select
RecordNumber,
ShoutOutID,
Description,
PostedByName,
PostedDate,
IM.ImageTitle,
IM.ImageBlob,
IM.Type,
T.BaseCommentID,
DisplayUserName,
IsVisible,
NotifyToShoutOutUser,
ShoutOutToUserAlias
from
#tempTable T
Left
join
dbo.
Image
IM
ON
IM.BaseCommentID
=
T.BaseCommentID
order
by
PostedDate
DESC
end
ELSE
begin
SELECT
*
FROM
#tempTable
order
by
PostedDate
DESC
end
SELECT
@ItemCount
=
Count
(
*
)
FROM
Shoutout
as
SO
JOIN
dbo.BaseComment
as
BC
ON
BC.ID
=
SO.BaseCommentID
WHERE
SO.LocationID
=
@LocationID
AND
BC.IsVisible
=
1
END
个人觉得ObjectDataSource控件是一个比较智能化的控件,它通过函数委托的方式自动执行用户提供的分页代码来完成数据库的“真分页”操作,省去了开发过程中的很多麻烦,还是很有必要去认真研究一下的。
ASP.NET_Pagination_Codes.rar