Asp.Net 分页显示控件

这篇文章最开始是2008年2月份写的,我起初想着大家应该都有自己的数据分页方式,而我本人并非专业做控件开发,所以只是想着提供点思路,并没有做太细致的研究。结果根据浏览量发现大家还是比较关注的,于是就抽空对它做了一点完善,对这篇文章也进行了一个更新。

介绍

借助 Asp.Net 提供的数据绑定控件,我们无需太多的代码,甚至不需要代码,只要在 VS2005 中拖拽几下控件,进行一些属性的设置,便可以实现在Asp时代需要做大量工作才能够实现的分页功能。但在实际的应用中,尤其是在Web站点程序中,我们经常需要更加漂亮美观的用户界面,而类似DataList或者 GridView 这样的数据控件往往不能或者很难满足我们的要求。此时,我们常常求助于 Repeater 控件,这样我们依旧会面临分页及其显示的问题。

本文不是讲述如何进行数据分页,而将注意力集中在如何根据记录总数、分页大小、当前所在页码,来在页面上显示出一个效果良好的分页链接,并提供一些简单的配置功能。所以,本文实现的控件功能 重在如何在界面上显示

而如何进行数据分页,可以参考这篇文章的姐妹篇:一步步构建“半自动”数据分页模块。在这篇文章中,我介绍了如何实现一个通用的数据分页模块,并与这个分页控件进行了一个结合。

关于本文所要说明的分页控件,大家可以先点击这个链接,看看实际的最终效果:

http://www.tracefact.net/Demo/Pager/Default.aspx

控件的制作思想

根据之前一些朋友对这个控件的一些反馈,我想先解释一点,就是它的制作思想:它是基于Http的Request请求,获取Url参数进行分页的;而不是集于Http的Post请求,获取隐藏的表单数据项来进行分页的

为什么我一开始就先要说清楚这个问题?因为有的朋友问我,为什么页面上有一个文本框,我在文本框中输入了内容,然后点击这个控件上的链接之后,文本框的内容没有了?这是因为当你点击控件上的链接以后,会重新发起一个Http的Request请求,相当于重新请求页面,所有状态均不保存。而你所期望的可以保存文本框内容的效果,是类似于.NET中GridView等控件的分页方式,它的实现是提交页面表单,将分页信息包含在表单项中。注意到使用这种方式分页时,页面的URL是不会发生变化的。

那么我为什么要采用这种每次都发起一个初始Requet请求的方式,而不是类似GridView的提交表单Post分页呢?因为这个控件设计用于前台用户界面,我希望通过一个初始的URL请求可以浏览到任意一页,比如说,如果URL为 www.tracefact.net/default.aps?page=3,那么我就可以直接访问default.aspx的第三页;但是使用提交表单式的分页方式时,你的URL就一个形式:www.tracefact.net/default.aspx,因为参数内容包含在表单项内,所以无法通过一个初始的URL请求访问页面的其他页,而只能是第一页(你可曾试过通过一次Request请求就访问GridView的任意一页?)。这样的话,对于Web站点来说是很不利的,因为我们希望页面的每页内容都可以被搜索引擎抓取到,用户就可以更多地利用搜索引擎来到站点,潜在地加大了站点的访问量。

控件组成

我们先做一点准备工作,新建一个空的解决方案,然后在其下添加一个Web站点项目WebSite,用于以后的测试;以及一个Asp.Net服务器控件项目ServerControl,它包含我们的分页显示控件。

UrlManager基类

开始之前,我们首先进行一个抽象,这个词经常出现,我对它的简单理解就是:提取相同和分离不同。想一想如果你在设计一个分页的显示控件,相同的是什么?稍微分析一下就可以得出,在页面显示分页链接时都需要当前页码、分页大小、页码总数,而 页码总数 通常是由另一个值――记录总数,以及 分页大小 计算得出的,并且计算方法相同。在采用URL进行分页时,当前页码是根据URL参数获得的,尽管参数名称可能各不相同,但是获取时所用的算法都相同。因此,我们可以把这些内容全部实现在一个基类中,我将它命名为了UrlManager。

我们再来看一下有什么是不同的,你的页面Url可能是Default.aspx?page=1 ,而另外一个站点的URL可能是这样的 Default.aspx?p=1,而对于一个使用了UrlMapping的站点来说,它的Url可能是Default-1.aspx。所以不同的是链接的URL各不相同。因此,我们在UrlManager中定义一个抽象方法GetPageUrl(int pageIndex),用于实现获取分页链接的算法。

这里需要注意的是,即使是类似Default-1.aspx这样应用了UrlMapping的页面,它获取参数时仍是采用Request. QueryString ["param"]这样的方式,所以在获取当前页码时,采用的方法与Default.asp?page=1并没有什么不同,仍属于相同的范围。

接下来我们看一下UrlManager基类的实现:

[Serializable]
public abstract class UrlManager {

    protected int currentPageIndex;         // 当前页码
    protected int recordCount;                  // 记录总数
    protected int pageSize;                     // 分页大小
    protected int pageCount;                    // 总页数
    protected string queryParam;                // 传递页数的参数名称
   
    protected UrlManager(int recordCount, int pageSize, string queryParam) {

        if (recordCount < 0)
            throw new ArgumentOutOfRangeException("recordCount 应该大于等于 0 !");
        if (pageSize <= 0)
            throw new ArgumentOutOfRangeException("pageSize 应该大于 0 !");
        if (String.IsNullOrEmpty(queryParam))
            throw new ArgumentNullException("queryParam 不能为空!");
       
        // 设置私有变量
        this.recordCount = recordCount;
        this.pageSize = pageSize;
        this.queryParam = queryParam;
        this.pageCount = getPageCount(recordCount, pageSize);
        this.currentPageIndex = getPageIndex(recordCount, pageCount, queryParam);
    }


    // 获取页码总数
    private int getPageCount(int recordCount, int pageSize) {
        int pageCount;

        // 如果记录数为0,也认为有一页(因为至少需要进行一个显示)
        if (recordCount == 0) {
            pageCount = 1;
        } else {
            // 计算总页数
            if (recordCount % pageSize == 0)
                pageCount = recordCount / pageSize;
            else
                pageCount = (recordCount / pageSize) + 1;
        }

        return pageCount;
    }
   
    // 从Url参数中获得但前页码
    private int getPageIndex(int recordCount, int pageCount, string queryParam) {
        if (recordCount == 0)
            return 1;       // 如果记录数为0,则显示为第一页

        int pageIndex;

        // 从Url参数获得当前页码
        string queryIndex =
            HttpContext.Current.Request.QueryString[queryParam];

        // 对页码进行一些校验
        if (string.IsNullOrEmpty(queryIndex))
            pageIndex = 1;      // 显示第一页
        else {
            try {
                pageIndex = Math.Abs(int.Parse(queryIndex));

                if (pageIndex == 0)
                    pageIndex = 1;

                // 如果当前页大于总页数,设当前页为最后一页
                if (pageIndex > pageCount)
                    pageIndex = pageCount;
            } catch {
                pageIndex = 1;  // 显示第一页
            }
        }
       
        return pageIndex;
    }

    public int PageCount {
        get { return pageCount; }
    }

    public int RecordCount {
        get { return recordCount; }
    }

    public int PageSize {
        get { return pageSize; }
    }

    public int CurrentPageIndex {
        get { return currentPageIndex; }
    }
    public abstract string GetPageUrl(int pageIndex);
}

DefaultUrlManager 类

从上面可以看出,UrlManager实现了大部分的逻辑,但是如何获取页面链接的方法GetPageUrl()没有实现,所以我们需要定义继承这个类的类型,然后在继承类中实现这个方法。为了使控件立即可用,我在这里提供了一个默认实现,我管它叫做 DefaultUrlManger。它通过Request.QueryString获取页码,其中Url的参数名称可以由自己指定,也可以使用默认的"Page"作为参数。

[Serializable]
public class DefaultUrlManager : UrlManager {

    public DefaultUrlManager(int recordCount, int pageSize, string queryParam)
        : base(recordCount, pageSize, queryParam) { }

    public DefaultUrlManager(int recordCount, int pageSize)
       : this(recordCount, pageSize, "Page") { }

    public DefaultUrlManager(int recordCount)
       : this(recordCount, 10) { }

    // 获得页面Url
    public override string GetPageUrl(int pageIndex) {
        string pageUrl = HttpContext.Current.Request.RawUrl;

        string pattern = @"(?<=[?&]"+ queryParam + @"=)(\d+)\b";
        Regex reg = new Regex(pattern, RegexOptions.IgnoreCase);
   
        // 如果找到匹配,也就是URL中含有类似 ?page=3 或者 &page=4 这样的字符串
        // 则对后面的数值进行替换
        if (reg.IsMatch(pageUrl)) {
            pageUrl = reg.Replace(pageUrl, pageIndex.ToString());
        } else {
            string queryString = HttpContext.Current.Request.Url.Query;

            if (string.IsNullOrEmpty(queryString))
                pageUrl += "?" + queryParam + "=" + pageIndex.ToString();
            else
                pageUrl += "&" + queryParam + "=" + pageIndex.ToString();
        }

        return pageUrl;
    }
}

在不使用UrlMapping的情况下,这个类已经足够用了。但如果你使用了UrlMapping,你需要重新定义一个继承自UrlManager的类,并且实现相应的GetPageUrl()方法。

PagerControl 类

如其名称所暗示的,这个类包含了实际在页面显示分页链接的逻辑,它必须接收一个UrlManager累不,因为在它内部会调用UrlManager的PageCount、CurrentPageIndex属性,以及GetPageUrl()方法,来在页面生成分页显示的HTML标记:

[DefaultProperty("Text")]
[ToolboxData("<{0}:PagerControl runat=server></{0}:PagerControl>")]
public class PagerControl : WebControl {

    private int currentPage;                    // 当前页的页码
    private int pageCount;                      // 总页数

    private bool showPrevious = false;      // 是否显示 上一页、第一页 的链接
    private bool showNext = false;          // 是否显示 下一页、最末页 的链接

    private int startPage;                      // 显示的第一页 的 页码
    private int endPage;                            // 显示的最末页 的 页码

    public PagerControl() {
        // 当前页之前可以显示的最多链接数,大于此条链接将被隐藏
        ViewState["PreviousPageCount"] = 5;

        // 当前页之后可以显示的最多链接数,大于此条链接将被隐藏
        ViewState["AfterPageCount"] = 4;

        // 供客户端设置样式
        ViewState["CssClass"] = "Pager";
    }

    public new string CssClass {
        get { return ViewState["CssClass"].ToString(); }
        set { ViewState["CssClass"] = value; }
    }
   
    public int PreviousPageCount {
        get { return (int)ViewState["PreviousPageCount"]; }
        set { ViewState["PreviousPageCount"] = value; }
    }

    public int AfterPageCount {
        get { return (int)ViewState["AfterPageCount"]; }
        set { ViewState["AfterPageCount"] = value; }
    }

    public UrlManager UrlManager {
        get { return (UrlManager)ViewState["UrlManager"]; }
        set { ViewState["UrlManager"] = value; }
    }

    // 添加“第一页”,“上一页”的连接
    private void AddPreviousLink(UrlManager UrlManager, HtmlTextWriter output) {

        output.AddAttribute(HtmlTextWriterAttribute.Class, "PagerIcon");
        output.AddAttribute(HtmlTextWriterAttribute.Title, "第一页");
        output.AddAttribute(HtmlTextWriterAttribute.Href, UrlManager.GetPageUrl(1));
        output.RenderBeginTag(HtmlTextWriterTag.A);
        output.Write("&lt;&lt;");
        output.RenderEndTag();
       
        output.AddAttribute(HtmlTextWriterAttribute.Class, "PagerIcon");
        output.AddAttribute(HtmlTextWriterAttribute.Title, "上一页");
        output.AddAttribute(HtmlTextWriterAttribute.Href, UrlManager.GetPageUrl(currentPage - 1));
        output.RenderBeginTag(HtmlTextWriterTag.A);
        output.Write("&lt;");
        output.RenderEndTag();

        showPrevious = false;   // 只显示一次
    }


    // 添加 “下一页”、“最末页” 的链接
    private void AddNextLink(UrlManager UrlManager, HtmlTextWriter output) {

        output.AddAttribute(HtmlTextWriterAttribute.Class, "PagerIcon");
        output.AddAttribute(HtmlTextWriterAttribute.Title, "下一页");
        output.AddAttribute(HtmlTextWriterAttribute.Href, UrlManager.GetPageUrl(currentPage + 1));
        output.RenderBeginTag(HtmlTextWriterTag.A);
        output.Write("&gt;");
        output.RenderEndTag();

        output.AddAttribute(HtmlTextWriterAttribute.Class, "PagerIcon");
        output.AddAttribute(HtmlTextWriterAttribute.Title, "最末页");
        output.AddAttribute(HtmlTextWriterAttribute.Href, UrlManager.GetPageUrl(pageCount));
        output.RenderBeginTag(HtmlTextWriterTag.A);
        output.Write("&gt;&gt;");
        output.RenderEndTag();

        showNext = false;   // 可有可无,程序会跳出循环
    }


    // 根据当前页,当前页之前可以显示的页数,算得从第几页开始进行显示
    private void SetStartPage() {

        // 如果当前页小于它前面所可以显示的条目数,
        // 那么显示第一页就是实际的第一页
        if (currentPage <= PreviousPageCount) {
            startPage = 1;
        } else 
            // 这种情况下 currentPage 前面总是能显示完,
            // 要根据后面的长短确定是不是前面应该多显示
    {
            if (currentPage > PreviousPageCount + 1)
                showPrevious = true;

            int linkLength = (pageCount - currentPage + 1) + PreviousPageCount;

            int startPage = currentPage - PreviousPageCount;

            while (linkLength < PreviousPageCount + AfterPageCount + 1 && startPage > 1) {
                linkLength++;
                startPage--;
            }

            this.startPage = startPage;
        }
    }

    // 根据CurrentPage、总页数、当前页之后长度 算得显示的最末页是 第几页
    private void SetEndPage() {
        // 如果当前页加上它之后可以显示的页数 大于 总页数,
        // 那么显示的最末页就是实际的最末页
        if (currentPage + AfterPageCount >= pageCount) {
            endPage = pageCount;
        } else {

            // 这种情况下 currentPage后面的总是可以显示完,
            // 要根据前面的长短确定是不是后面应该多显示

            int linkLength = (currentPage - startPage + 1) + AfterPageCount;

            int endPage = currentPage + AfterPageCount;

            while (linkLength < PreviousPageCount + AfterPageCount + 1 && endPage < pageCount) {
                linkLength++;
                endPage++;
            }

            if (endPage < pageCount)
                showNext = true;

            this.endPage = endPage;
        }
    }


    // 显示在页面上
    protected override void Render(HtmlTextWriter output) {

        output.AddAttribute(HtmlTextWriterAttribute.Class, CssClass);          
        output.RenderBeginTag(HtmlTextWriterTag.Div);

        if (UrlManager == null)
            throw new ArgumentNullException("UrlManager 不能为 Null");

        // 获取当前页
        currentPage = UrlManager.CurrentPageIndex;

        // 获取总页数
        pageCount = UrlManager.PageCount;

        SetStartPage();
        SetEndPage();

        // 循环打印链接
        for (int i = startPage; i <= endPage; i++) {
            if (showPrevious)           // 如果需要显示前一页、第一页链接
                AddPreviousLink(UrlManager, output);

           
            if (i == currentPage) {
                output.AddAttribute(HtmlTextWriterAttribute.Class, "CurrentPage");
            }

            output.AddAttribute(HtmlTextWriterAttribute.Href, UrlManager.GetPageUrl(i));
            output.RenderBeginTag(HtmlTextWriterTag.A);
            output.Write(i);
            output.RenderEndTag();  // A

            if (i == endPage && showNext)   // 如果需要显示 下一页、最末页 链接
                AddNextLink(UrlManager, output);
        }

        output.RenderBeginTag(HtmlTextWriterTag.Span);
        output.Write(String.Format(" ( 第<b>{0}</b>页/共<b>{1}</b>页 )", currentPage, pageCount));
        output.RenderEndTag();  // Span

        output.RenderEndTag();  // Div
    }

}

这个类实现的分页显示效果就是你在文章一开始所看到的效果演示。所以在这里我就再不对其显示效果进行说明了,而在代码中也包含了大量的注释,相信很容易就能看得明白。

设置样式

控件没有提供任何的样式控制,对于样式,你唯一能做的就是通过它的CssClass属性来设置控件生成的Div的Class,然后利用这个Class编写样式表来控制显示。如果有必要,你还可以通过利用PagerIcon这个Css类来控制“上一页”、“下一页”、“第一页”、“最末页”的显示;通过 CurrentPage 这个Css类来控制 当前页 的显示。由此,在所有使用这个控件的页面,你都应该包含有控制控件样式的样式表。

这里,我提供了一个默认的实现(在你不设置控件的CssClass属性的时候,默认为Pager):

.Pager a{
    display:block;
    border:1px solid #ccc;
    float:left;
    padding:4px 5px;
    text-decoration:none;
    text-align:center;
    margin:0 1px;
}

.Pager a.CurrentPage{
    background:#999;
    color:#eee;
}

.Pager span{
    position:relative;
    top:6px;
}

控件的使用

好了,现在一切都OK了,让我们看看控件如何使用。我们以一种最简单的方式开始,再以一种最复杂的方式开始。

声明式使用

直接拖拽控件到页面上(比如Default.aspx),然后在CodeBehind 中设置一下它的RecordCount值就可以了。aspx页面代码如下:

// ... 略
<%@ Register assembly="ServerControl" namespace="ServerControl" tagprefix="cc1" %>
// ... 略
<cc1:PagerControl ID="CustomPager1" runat="server" />

代码后置文件的内容如下(片段):

if(!IsPostBack){
    // 最简单的使用方式
    CustomPager1.UrlManager = new DefaultUrlManager(337);
}

动态创建式使用

我们也可以编写后置代码,然后来动态地使用它。为了看一下它如何配合Repeater控件使用,我们再在页面上拖一个Repeater控件,采用默认的命名Repeater1; GetList()方法返回一个列表,我们将对这个列表进行分页显示(下载 完整代码):

protected void Page_Load(object sender, EventArgs e) {

    // 获取数据
    List<DemoObj> list = GetList();

    // 动态使用方式
    PagerControl pager = new PagerControl();

    // 设置根据Request.QueryString获取页码的参数
    DefaultUrlManager manager = new DefaultUrlManager(list.Count, 7, "P");
    pager.UrlManager = manager;     // 因为 HttpContext 的原因 UrlManager 不可串行化

    // 如果你实现了自己的IUrlManager接口,这里可能是这样:
    // pager.UrlManager = new YourUrlManger(133);          

    pager.CssClass = "GreenStyle"// 设置颜色 
    pager.PreviousPageCount = 3;        // 设置当前页之前显示的最大链接数
    pager.AfterPageCount = 3;           // 设置当前也之后可以显示的最大链接数

    // 将控件加入到页面上
    phHolder.Controls.Add(pager);

    // 使用 GetRange() 方法进行分页
    PagerAdapter<DemoObj> adpter = new PagerAdapter<DemoObj>(list);
    Repeater1.DataSource = adpter.GetRange(manager);
    Repeater1.DataBind();
}

这里要注意两个地方:首先,因为是动态创建的控件,所以不能将它放置在if(!IsPostBack){}中。其次,由于我们使用了新的CssClass样式,所以你也需要提供基于GreenStyle的样式表,我是这样提供的:

.GreenStyle a{
    display:block;
    border:1px solid #9cba39;
    float:left;
    padding:4px 5px;
    text-decoration:none;
    text-align:center;
    margin:0 1px;
    color:#9cba39
}

.GreenStyle a.CurrentPage{
    background:#C5D985;
    color:#fff;
}

.GreenStyle span{
    position:relative;
    top:6px;
}

.GreenStyle span b{
    color:#C33;
}

总结

本文我们实现了Asp.Net中的一个常见的功能,通过一个用户控件来实现数据分页的页面层以达到代码重用的目的。

我们使用UrlManager来实现了大部分的基础逻辑,包括获得当前页数、计算分页总数等,接着定义了一个默认的DefaultUrlManager,它使用Url参数来获得当前页码,并实现了GetPageUrl()方法,用于生成分页的链接。然后我们实现了控件的主要代码,它的主要功能就是根据UrlManager提供的参数和方法来生成分页链接的HTML代码。最后,我们分别使用页面声明和动态创建两种方式使用了这个控件。

如同前面所说,这个控件注重在页面显示分页链接,而不涉及如何从数据库表中获取某页的数据,在它的姐妹篇――一步步构建“半自动”数据分页模块,我讨论了数据分页的话题,并与这个控件结合了起来。

感谢你阅读本文,希望这篇文章能带给你帮助。