先說說數據分頁的原理。我們在開發數據綁定頁面的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控件是一個比較智能化的控件,它通過函數委托的方式自動執行用戶提供的分頁代碼來完成數據庫的“真分頁”操作,省去了開發過程中的很多麻煩,還是很有必要去認真研究一下的。