前台:浏览一个Thread的内容
http://192.168.18.153:81/forums/t/3.aspx?PageIndex=1
其中的分页部分,是我们要研究的
根据网址,实际上是用了正则表达式。
查看SiteUrls.config,找到
<url name="thread_Paged" path="t/{0}.aspx?PageIndex={1}" pattern="t/(\d+).aspx?p=(\d+)" physicalPath="##themeDir##" vanity="{2}?ThreadID=$1^PageIndex=$2" page="thread.aspx" />
<url name="thread" path="t/{0}.aspx" pattern ="t/(\d+).aspx" physicalPath="##themeDir##" vanity="{2}?ThreadID=$1" page="thread.aspx" />
这里匹配第1个,再细节,我也不考虑了,最终应该定位到
/Theme/Default/Forums/thread.aspx页面,这与你的Theme目录有关系。
void Page_Init()
{
if (CurrentThread != null)
{
if (ChangeViewPopupMenu.GetPostViewType(CurrentThread) == PostViewType.Threaded)
ViewContainer.Controls.Add(Page.LoadControl("thread-threadedview.ascx"));
else
ViewContainer.Controls.Add(Page.LoadControl("thread-flatview.ascx"));
}
在Page_Init中会根据当前的浏览类型,决定是以 树型结构还是 平板结构 显示页面
以Flatview形式讨论:
再查看thread-flatview.ascx页面,这是一个用户控件
<div class="CommonFormArea">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td colspan="2" align="right" class="CommonFormField CommonPrintHidden">
<CSControl:Pager runat="server" id="TopPager" ShowTotalSummary="true" Tag="Div" CssClass="CommonPagingArea" align="right" />
</td>
</tr>
注意,这里有个<CSControl:Pager>用户控件,id为TopPager
在页面的下端,
<CSControl:PagerGroup runat="server" Id="Pager" PagerIds="TopPager,BottomPager" />
<CSControl:Pager runat="server" id="BottomPager" ShowTotalSummary="true" Tag="Div" CssClass="CommonPagingArea" align="right" />
这里定义了<CSControl:PagerGroup> 和<CSControl:Pager>,从字面意思,PageGroup控制所有的Pager。
从官方网站,有篇文章专门讲述了一个CS Pager
<CSForum:ForumPostList runat="server" ID="PostList">
<QueryOverrides SortBy="PostDate" PagerID="Pager" />
<HeaderTemplate><ul class="ForumPostList"></HeaderTemplate>
<ItemTemplate>
从这里开始,主要就是显示Post列表了,用了一个Table,左边一列显示 ForumPostUserArea
右边一列分成两行,分别显示ForumPostTitleArea和 ForumPostBodyArea
如果查看CSForm:ForumPostList控件
在CommunityServerForums20项目下 Controls\ForumPost\ForumPostList.cs 源程序
在public override object DataSource 函数中有
query.PageIndex = 0;
query.PageSize = ForumConfiguration.Instance().PostsPerPage;
if (this.QueryOverrides != null)
this.QueryOverrides.ApplyQueryOverrides(query);
if (query.PostID > 0)
{
PostSet posts = Posts.GetPosts(query);
_forumPosts = new List<ForumPost>();
foreach (ForumPost p in posts.Posts)
_forumPosts.Add(p);
if (this.QueryOverrides != null && this.QueryOverrides.Pager != null)
{
this.QueryOverrides.Pager.PageIndex = query.PageIndex;
this.QueryOverrides.Pager.PageSize = query.PageSize;
this.QueryOverrides.Pager.TotalRecords = posts.TotalRecords;
this.QueryOverrides.Pager.OnPageIndexChanged += new PagerEventHandler(this.PageIndexChanged);
this.QueryOverrides.Pager.DataBind();
}
}
else if (this.QueryOverrides != null && this.QueryOverrides.Pager != null)
{
this.QueryOverrides.Pager.TotalRecords = 0;
this.QueryOverrides.Pager.DataBind();
}
当然,我们可以看到CS2007中用了泛型,这与2.1不同,2.1还是用了ArrayList,这样性能肯定能得到很大的提升,少了装箱拆箱环节。
PostSet posts = Posts.GetPosts(query);这一句比较重要
查看一下定义
public static PostSet GetPosts(ForumPostQuery query)
{
return GetPosts(query.PostID, query.PageIndex, query.PageSize, query.SortBy, query.SortOrder);
}
最终定位到这样一个方法
// *********************************************************************
// GetPosts
//
/// <summary>
/// This method returns a listing of the messages in a given thread using paging.
/// </summary>
/// <param name="PostID">Specifies the PostID of a post that belongs to the thread that we are
/// interested in grabbing the messages from.</param>
/// <returns>A PostCollection containing the posts in the thread.</returns>
///
// ********************************************************************/
public static PostSet GetPosts(int postID, int pageIndex, int pageSize, int sortBy, int sortOrder, bool includeCategories)
{
PostSet postSet = null;
string key = "Forum-Posts::P:{0}-PI:{1}-PS:{2}-SB:{3}-SO:{4}-C:{5}";
string postCollectionKey = string.Format(key,postID,pageIndex,pageSize, sortBy, sortOrder, includeCategories);
CSContext csContext = CSContext.Current;
postSet = csContext.Items[postCollectionKey] as PostSet;
if(postSet == null)
postSet = CSCache.Get(postCollectionKey) as PostSet;
if (postSet == null) {
// Create Instance of the CommonDataProvider
ForumDataProvider dp = ForumDataProvider.Instance();
postSet = dp.GetPosts(postID, pageIndex, pageSize, sortBy, sortOrder, CSContext.Current.User.UserID, true, includeCategories);
csContext.Items[postCollectionKey] = postSet;
if(pageIndex == 0)
CSCache.Insert(postCollectionKey,postSet,6);
}
return postSet;
}
先从Content取,取不到再到Cache取,再取不到,才到数据库取。
从这里可以看出,CS充分考虑了性能,运用缓存和分页
ForumDataProvider dp = ForumDataProvider.Instance();
postSet = dp.GetPosts(postID, pageIndex, pageSize, sortBy, sortOrder, CSContext.Current.User.UserID, true, includeCategories);
这一段用了一个Provider模式,具体的实现在SqlDataProvider20项目中,这里主要考虑跨数据库。CS也充分运用了设计模式,的确,老外写的程序真的不一样,不是我崇洋媚外,我是发自内心的。
写到这里,谈谈国货,特别是手机,国产的就不要买了。要买就买Nokia,上次移动充话费,300送个Nokia 1116,唉,我第1个手机是Nokia 3110,到现在还很好用,还给我岳父用着。如果你没钱,就用移动送的,再不行,就买个最便宜的Nokia,比什么都好,有钱,就买Nokia价位好点的,它的质量你绝对不要怀疑。
从高到低通吃,想不占领市场都难。
再查看GetPosts的具体实现
在SqlDataProvider20项目下的 ForumsSqlDataProvider.cs类中
#region GetPosts
/// <summary>
/// Returns a collection of Posts that make up a particular thread with paging
/// </summary>
/// <param name="postID">The ID of a Post in the thread that you are interested in retrieving.</param>
/// <returns>A PostCollection object that contains the posts in the thread.</returns>
///
public override PostSet GetPosts(int postID, int pageIndex, int pageSize, int sortBy, int sortOrder, int userID, bool returnRecordCount, bool includeCategories)
{
// Create Instance of Connection and Command Object
//
using( SqlConnection myConnection = GetSqlConnection() )
{
SqlCommand myCommand = new SqlCommand(databaseOwner + ".cs_forums_Posts_PostSet", myConnection);
myCommand.CommandType = CommandType.StoredProcedure;
PostSet postSet = new PostSet();
// Set parameters
//
myCommand.Parameters.Add("@PostID", SqlDbType.Int).Value = postID;
myCommand.Parameters.Add("@PageIndex", SqlDbType.Int).Value = pageIndex;
myCommand.Parameters.Add("@PageSize", SqlDbType.Int).Value = pageSize;
myCommand.Parameters.Add("@SortBy", SqlDbType.Int).Value = sortBy;
myCommand.Parameters.Add("@SortOrder", SqlDbType.Int).Value = sortOrder;
myCommand.Parameters.Add("@UserID", SqlDbType.Int).Value = userID;
myCommand.Parameters.Add("@ReturnRecordCount", SqlDbType.Bit).Value = returnRecordCount;
myCommand.Parameters.Add("@IncludeCategories", SqlDbType.Bit).Value = includeCategories;
myCommand.Parameters.Add(this.SettingsIDParameter());
// Execute the command
//
myConnection.Open();
using(SqlDataReader reader = myCommand.ExecuteReader(CommandBehavior.CloseConnection))
{
// Get the results
//
IList<int> authors = new List<int>();
while (reader.Read())
{
authors.Add(Convert.ToInt32(reader["UserID"]));
postSet.Posts.Add(PopulatePostFromIDataReader(reader));
}
Users.AddUsersToCache(authors);
if (includeCategories)
{
reader.NextResult();
Hashtable categoryLookupTable = new Hashtable();
while(reader.Read())
{
int cPostID = (int) reader["PostID"];
if (categoryLookupTable[cPostID] == null)
{
categoryLookupTable[cPostID] = new ArrayList();
}
((ArrayList) categoryLookupTable[cPostID]).Add(reader["Name"]);
}
// Map categories to the threads
//
foreach (ForumPost post in postSet.Posts)
{
if (categoryLookupTable.ContainsKey(post.PostID))
post.Categories = (string[]) ((ArrayList) categoryLookupTable[post.PostID]).ToArray(typeof(string));
}
}
// Are we expecting more results?
//
if ((returnRecordCount) && (reader.NextResult()) )
{
reader.Read();
// Read the value
//
postSet.TotalRecords = (int) reader[0];
}
reader.Close();
}
myConnection.Close();
return postSet;
}
}
用了存储过程cs_forums_Posts_PostSet
查看定义
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[cs_forums_Posts_PostSet]
(
@PostID int,
@PageIndex int,
@PageSize int,
@SortBy int,
@SortOrder bit,
@UserID int,
@ReturnRecordCount bit,
@AllowUnapproved bit = 0,
@SettingsID int,
@IncludeCategories bit = 0
)
AS
SET Transaction Isolation Level Read UNCOMMITTED
BEGIN
DECLARE @PageLowerBound int
DECLARE @PageUpperBound int
DECLARE @ThreadID int
DECLARE @SectionID int
-- First set the rowcount
DECLARE @RowsToReturn int
SET @RowsToReturn = @PageSize * (@PageIndex + 1)
SET ROWCOUNT @RowsToReturn
-- Set the page bounds
SET @PageLowerBound = @PageSize * @PageIndex
SET @PageUpperBound = @PageLowerBound + @PageSize + 1
-- Get the ThreadID
SELECT
@ThreadID = ThreadID,
@SectionID = SectionID
FROM
cs_Posts
WHERE
PostID = @PostID and SettingsID = @SettingsID
-- Is the Forum 0 (If so this is a private message and we need to verify the user can view it
IF @SectionID = 0
BEGIN
IF NOT EXISTS (SELECT UserID FROM cs_PrivateMessages WHERE UserID = @UserID AND ThreadID = @ThreadID AND SettingsID = @SettingsID)
RETURN
END
-- Create a temp table to store the select results
CREATE TABLE #PageIndex
(
IndexID int IDENTITY (1, 1) NOT NULL,
PostID int
)
-- Sort by Post Date
IF @SortBy = 0 AND @SortOrder = 0
INSERT INTO #PageIndex (PostID)
SELECT PostID FROM cs_Posts (nolock) WHERE (IsApproved = 1 OR 1 = @AllowUnapproved) AND ThreadID = @ThreadID and SettingsID = @SettingsID ORDER BY PostDate
ELSE IF @SortBy = 0 AND @SortOrder = 1
INSERT INTO #PageIndex (PostID)
SELECT PostID FROM cs_Posts (nolock) WHERE (IsApproved = 1 OR 1 = @AllowUnapproved) AND ThreadID = @ThreadID and SettingsID = @SettingsID ORDER BY PostDate DESC
-- Sort by Author
IF @SortBy = 1 AND @SortOrder = 0
INSERT INTO #PageIndex (PostID)
SELECT PostID FROM cs_Posts (nolock) WHERE (IsApproved = 1 OR 1 = @AllowUnapproved) AND ThreadID = @ThreadID and SettingsID = @SettingsID ORDER BY UserID
ELSE IF @SortBy = 1 AND @SortOrder = 1
INSERT INTO #PageIndex (PostID)
SELECT PostID FROM cs_Posts (nolock) WHERE (IsApproved = 1 OR 1 = @AllowUnapproved) AND ThreadID = @ThreadID and SettingsID = @SettingsID ORDER BY UserID DESC
-- Sort by SortOrder
IF @SortBy = 2 AND @SortOrder = 0
INSERT INTO #PageIndex (PostID)
SELECT PostID FROM cs_Posts (nolock) WHERE (IsApproved = 1 OR 1 = @AllowUnapproved) AND ThreadID = @ThreadID and SettingsID = @SettingsID ORDER BY SortOrder
ELSE IF @SortBy = 2 AND @SortOrder = 1
INSERT INTO #PageIndex (PostID)
SELECT PostID FROM cs_Posts (nolock) WHERE (IsApproved = 1 OR 1 = @AllowUnapproved) AND ThreadID = @ThreadID and SettingsID = @SettingsID ORDER BY SortOrder DESC
-- Select the individual posts
SELECT
P.PostID, P.ThreadID, P.ParentID, P.PostAuthor, P.UserID, P.SectionID, P.PostLevel, P.SortOrder, P.Subject, P.PostDate, P.IsApproved,
P.IsLocked, P.IsIndexed, P.TotalViews, P.Body, P.FormattedBody, P.IPAddress, P.PostType, P.PostMedia, P.EmoticonID, P.SettingsID, P.AggViews,
P.PropertyNames as PostPropertyNames, P.PropertyValues as PostPropertyValues,
P.PostConfiguration, P.UserTime, P.ApplicationPostType, P.PostName, P.PostStatus, P.SpamScore,
P.Points as PostPoints, P.RatingSum as PostRatingSum, P.TotalRatings as PostTotalRatings,
T.*, U.*, #PageIndex.*,
T.IsLocked,
T.IsSticky,
Username = P.PostAuthor,
ThreadStarterAuthor = T.PostAuthor,
ThreadStartDate = T.PostDate,
EditNotes = (SELECT EditNotes FROM cs_PostEditNotes WHERE PostID = P.PostID),
AttachmentFilename = ISNULL ( (SELECT [FileName] FROM cs_PostAttachments WHERE PostID = P.PostID), ''),
Replies = 0, --not used(SELECT COUNT(P2.PostID) FROM cs_Posts P2 (nolock) WHERE P2.ParentID = P.PostID AND P2.PostLevel != 1),
IsModerator = 0, -- not used
HasRead = 0 -- not used
FROM
cs_Posts P (nolock),
cs_Threads T,
cs_vw_Users_FullUser U,
#PageIndex
WHERE
P.PostID = #PageIndex.PostID AND
P.UserID = U.UserID AND
T.ThreadID = P.ThreadID AND
#PageIndex.IndexID > @PageLowerBound AND
#PageIndex.IndexID < @PageUpperBound and U.SettingsID = @SettingsID
ORDER BY
IndexID
END
IF @IncludeCategories = 1
BEGIN
SET ROWCOUNT 0
SELECT
Cats.[Name], jPI.PostID
FROM
#PageIndex jPI
JOIN cs_Posts_InCategories
JOIN cs_Post_Categories Cats ON PIC.CategoryID = Cats.CategoryID
WHERE
jPI.IndexID > @PageLowerBound
AND jPI.IndexID < @PageUpperBound
AND PIC.SettingsID = @SettingsID
End
IF @ReturnRecordCount = 1
SELECT count(PostID) FROM cs_Posts (nolock) WHERE (IsApproved = 1 OR 1 = @AllowUnapproved) AND ThreadID = @ThreadID and SettingsID = @SettingsID
可以看到,SET ROWCOUNT @RowsToReturn,临时表,连接查询,分页算法,在大数据量时,应该也不错,最后一句返回记录总数。但现在SQL 2005支持RowNum,我上网看了一下,赞扬和批评的声音都有。分页确实很重要,我现在优化海学网,最先想到的就是分页和缓存。先研究一下分页。
上层程序通过SqlDataReader reader和reader.NextResult(),封装返回的结果集和记录总数。
底层实现差不多就这样,现在再看上层展示。
public class ForumPostList : PreTemplatedListBase
public abstract class PreTemplatedWrappedRepeaterBase : WrappedRepeater
public class WrappedRepeater : Repeater, IAttributeAccessor
最终我们可以看到ForumPostList扩展了Repeater控件
在PreTemplatedWrappedRepeaterBase类的
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (!_isDataBound)
DataBind();
}
最终会调用DataBind()函数,实现页面数据绑定
在ForumPostList中改写DataSource函数。
在<CSForum:ForumPostList runat="server" ID="PostList">
<QueryOverrides SortBy="PostDate" PagerID="Pager" />控件中
加入了一个QueryOverrides 属性
public virtual ForumPostQuery QueryOverrides
{
get { return _queryOverrides; }
set
{
if (_queryOverrides != null)
this.Controls.Remove(_queryOverrides);
_queryOverrides = value;
if (_queryOverrides != null)
this.Controls.Add(_queryOverrides);
}
}
这个属性是ForumPostQuery控件
public class ForumPostQuery : System.Web.UI.Control,
本质上这是一个传递属性字段的容器,这是我的理解
if (this.QueryOverrides != null && this.QueryOverrides.Pager != null)
{
this.QueryOverrides.Pager.PageIndex = query.PageIndex;
this.QueryOverrides.Pager.PageSize = query.PageSize;
this.QueryOverrides.Pager.TotalRecords = posts.TotalRecords;
this.QueryOverrides.Pager.OnPageIndexChanged += new PagerEventHandler(this.PageIndexChanged);
this.QueryOverrides.Pager.DataBind();
}
this.QueryOverrides.Pager.DataBind();这句话是在绑定分页控件
查来查去
在public abstract class WrappedContentBase : Control, IAttributeAccessor, INamingContainer
抽象类中有这样一个方法,调用CreateChildControls(),这个方法会一层一层调用,最终调用
public abstract class PreTemplatedPagerBase : WrappedContentBase, IPager, IDataItemsContainer
这个类的protected override void BindDefaultContent(Control control, IDataItemContainer dataItemContainer)方法,这样,我们的分页控件的内容就输出来了。
protected override void OnDataBinding(EventArgs e)
{
if (RecreateChildControlsOnDataBind)
{
this.ChildControlsCreated = true;
this.CreateChildControls();
}
else
EnsureChildControls();
base.OnDataBinding(e);
}
在public class PagerGroup : Control, IPager
类中用了代理,
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
this.Page.InitComplete += new EventHandler(Page_InitComplete);
}
void Page_InitComplete(object sender, EventArgs e)
{
string[] ids = this.PagerIds.Split(',');
foreach (string id in ids)
{
IPager pager = CSControlUtility.Instance().FindControl(this, id) as IPager;
if (pager != null)
pager.OnPageIndexChanged += new PagerEventHandler(PagerGroup_OnPageIndexChanged);
}
}
当里面的任何一个分页控制,发生页数更改时,就会调用这个Group类,Group再修改所有分页控件的样式。因为分页是采用超链接,我想这个代理仅会发生在页面载入时。
这里是“窥一斑而见全豹”,还有很多要学的,再看看吧。
再来分析后台分页功能,后台用了
<add tagPrefix="CA" namespace="ComponentArt.Web.UI" assembly="ComponentArt.Web.UI"/> 的Grid控件。
其实这里没什么好说的,取数据还是存储过程取,表现用了ComponentArt控件,就方便多了,实现的原理和ComponentArt自带的例子里的一个Manual Page一样,就是手工分页。