Click here to Skip to main content
15,884,872 members
Articles / Web Development / ASP.NET
Article

Client-Side Paging with Tables

Rate me:
Please Sign up or sign in to vote.
3.69/5 (13 votes)
6 Feb 20056 min read 115.2K   4K   48   11
A very simple, cross-browser approach to client-side paging.

Re-Introduction

In the first iteration of this article, I demonstrated how to use HTML tables on the client for a very simple client-side paging solution. I have heard from several people who point out the performance problems with large sets of data. I agree. This solution is best for a fairly fixed amount of data. It's suited for a commercial application generally having between eight and twelve pages with about five "rows" or embedded tables per page.

Still the only performance hit is on page load. The client-side processing is very fast, even on slower machines. On a P-III 700 Mhz with 256 MB RAM, it responds about as well as it does on my 1.8 Ghz P-4 with 512 MB.

The ASP.NET DataGrid, and third party, commercial extensions thereof, make developers’ lives easier in many ways. But the DataGrid is a crutch, and, while it may allow you to walk, it hinders you from running. Sometimes, a project arises with technical and user requirements that make the DataGrid both too heavy and too limited. It was just such a project that forced me to leave, temporarily, the drag-and-drop development world for some code that would meet all of my client’s needs.

Background

My recent project for a well-known company required displaying a fairly large number of similar datasets involving marketing plans. Basically, users needed to see a list of their plans with buttons to take them to an edit page, buttons to expose context help inline with the form, and several rows of data for each plan. Moreover, the client demanded that postbacks be kept to a minimum, and expressly rejected my original plan posting back to change pages and to change the dropdownlist content based on other dropdownlist selections.

I solved the latter problem by extending Al Alberto’s Master Detail DDL project, which I found here on Code Project. But the paging remained a problem. I tried various techniques with DataGrids and Repeaters, but all were too complex, requiring hundreds or more lines of code than I knew the solution really needed. Among those that provided some excellent insight into paging was Andrew Merlino’s article and code.

I realized that HTML tables can work just like div layers. By setting a table’s style=”display:none” property, I can hide the table on the client. All I needed to do was to create one table for each page. These tables contained a header row with a single cell, and a content row with a single cell. Into the content cell, I can inject anything—in the case of my demo, a series of tables containing data and formatting information. All that remained was to create some dynamic JavaScript to handle the page change requests on the client.

My original article was stark. In this iteration, I've concentrated on a few of the user features that make this approach desirable.

First, I switched databases from Northwind to pubs. This allows me to use richer data with more detail. To make the application more portable, I added the connection string to the web.config file.

Next, I've added progress bars for each "row" which roll up to an overall progress roll on the main page. Additionally, I've added an update field to allow users to set sales targets.

Finally, to make updates faster, I've added a psuedo-spin control to edit mode that moves the edit window one row up or down. When you reach the end of a page, it automatically takes you to the first row of the next page or the last row of the previous page.

Using the code

My demonstration, while admittedly sparse, consists of a single aspx page with its code-behind, containing fewer than 250 lines of code. The steps are as simple as the solution itself:

  • Get a DataSet containing a single DataTable.
  • Determine the number of pages I need based on a user-definable Page Size and the number of rows in my table.
  • Create a Container table for each page.
  • Create a child table for each row of data.
  • Add the child table to the Container table.
  • Add the Container tables to the page, in this case, by adding it to the Controls element of a table cell set to runat=”server”.
  • Add the JavaScript to the page.

For this demo, I used the Northwind database in SQL Server. You can easily change the connection string/query string to use any SQL Server database you desire.

C#
private void BuildTables ()
{
    // Determine total number of records
    int NumItems  = ds.Tables[0].Rows.Count;

    // Set number of records per page
    int PageSize  =    Int32.Parse(txtPageSize.Text);
    // Determine    number of pages minus any leftover records
    long Pages  = (NumItems    / PageSize);
    // Save this number for future reference

    long WholePages =  NumItems / PageSize;
    // Determine number of leftover records

    int Leftover =  NumItems  % PageSize;
    //If there are leftover records, increase page count by one

    if (Leftover >  0)
    { 
        Pages += 1; 
    } 

    int StartOfPage = 0; 
    int EndOfPage = PageSize -1;
    this.lblPages.Text = Pages.ToString() + " Pages";
    this.lblRecords.Text =    NumItems.ToString() + "Records";

    for(intp=1;p<=Pages;p++)
    {
        //Create Page Tables
        HtmlTable tblPage = new HtmlTable();
        HtmlTableRow trow = new HtmlTableRow();
        HtmlTableRow hrow = new HtmlTableRow();
        HtmlTableCell hcell = new HtmlTableCell();
        hcell.InnerHtml = "<b>Page " +p.ToString()+"</b>";
        hrow.Cells.Add(hcell);
        tblPage.Rows.Add(hrow);
        HtmlTableCell tcell = new HtmlTableCell();
        tblPage.ID = "Page"+p;
        if(p==1)
            tblPage.Style.Add("Display","block");
        else
            tblPage.Style.Add("Display","none");
        for(int i = StartOfPage;i<=EndOfPage;i++)
        {
            if(i < ds.Tables[0].Rows.Count)
            {
                tcell.Controls.Add(FillPages(ds.Tables[0].Rows[i],i,p));
                tcell.Controls.Add(BuildEditTable(ds.Tables[0].Rows[i],i,p));
            }
        }
        trow.Cells.Add(tcell);
        tblPage.Rows.Add(trow);
        HtmlTableCell tdPages = (HtmlTableCell)FindControl("tdPages");
        tdPages.Controls.Add(tblPage);
        StartOfPage = EndOfPage+1;
        EndOfPage = EndOfPage+PageSize;
    }

    OverallProgress = (OverallProgress/NumItems);
    this.hdnOverallPercent.Value=OverallProgress.ToString()+"%";
    this.ovl.InnerHtml = OverallProgress.ToString()+"%";

    this.RenderScript(Convert.ToInt32(Pages), Convert.ToInt32(NumItems));
}

The method above calls the FillPages (int Record) method iteratively, passing the current row value and receiving a table in return. There are other, more elegant ways of determining how many rows are in the DataSet table, but, again, the goal of this project is simplicity.

New to this iteration is the logic for determining progress and displaying a portion of a GIF image proportionate to the progress. Again, the principle is very simple: the image is 100px. I determine the percent completed (actual/target) and set the image width to equal the percent completed. If the Actual is 30 and the Target is 100, the image's width is 30px, or 30% of the complete picture.

I also keep a running total of the percentage for the overall progress. To keep things simple and on the client, I use a hidden HTML field (HtmlInputHidden) to store the running total. After building the pages, I'll divide the number of records by the sum of PercentComplete for the overall progress.

C#
private HtmlTable FillPages(DataRow Record, int tableNumber, int pageNumber)
{
    HtmlTable tblNew = new HtmlTable();
    HtmlTableRow r1 = new HtmlTableRow();
    HtmlTableRow r2 = new HtmlTableRow();
    HtmlTableCell c1 = new HtmlTableCell();
    HtmlTableCell c2 = new HtmlTableCell();
    HtmlTableCell c3 = new HtmlTableCell();
    HtmlTableCell c4 = new HtmlTableCell();
    HtmlInputButton b1 = new HtmlInputButton();

    b1.Value="Edit";
    b1.ID="b"+tableNumber.ToString();
    b1.Attributes.Add("onclick","doEdit('"+b1.ID+"')");
    c4.Controls.Add(b1);
    //format

    tblNew.Style.Add("DISPLAY","block");
    tblNew.ID="#vw"+tableNumber.ToString();
    tblNew.Attributes.Add("Page",pageNumber.ToString());
    tblNew.Border=1;
    tblNew.CellSpacing=0;
    tblNew.CellPadding=3;
    tblNew.Width = "520px";

    c1.BgColor="silver";
    c1.Style.Add("FONT-COLOR","WHITE");
    c2.Width="80%";
    c3.Width = "20%";
    //fill cells
    int intProgress = 
      Convert.ToInt32(Double.Parse(Record[4].ToString())/
      Double.Parse(Record[5].ToString())*100);
    if(intProgress>100)
        intProgress=100;
    this.OverallProgress += intProgress;
    c2.InnerHtml = "Title: "+Record[1].ToString() + 
            "<br>Sales: $" +Record[4] + "     Target: $" + Record[5];
    c1.InnerHtml = "<b>" + Record[0].ToString() + 
      "</b>     Progress" + 
      "   <span id='Label7' " + 
      "class='TaskProgress' ></span>" + 
      " "+intProgress.ToString()+ "%" + b1;

    //c2.InnerHtml = "Title: "+Record[1].ToString() 
    //   + "<br>Sales: $" +Record[4] + "     Target: $" + Record[5];
    c3.InnerHtml = "Category: " +Record[2].ToString();
    //assign cells to rows
    r1.Cells.Add(c1);
    r1.Cells.Add(c4);
    r2.Cells.Add(c2);
    r2.Cells.Add(c3);
    //assign rows to table
    tblNew.Rows.Add(r1);
    tblNew.Rows.Add(r2);
    return tblNew;
}

Now, we're going to build the Edit table. Because I am using the Pubs database, I do not store any changes made on the page to the database, so please don't expect your changes to persist.

C#
private HtmlTable BuildEditTable(DataRow Record, int tableNumber, int pageNumber)
{
    HtmlTable tblNew = new HtmlTable();
    HtmlTableRow r1 = new HtmlTableRow();
    HtmlTableRow r2 = new HtmlTableRow();
    HtmlTableCell c1 = new HtmlTableCell();
    HtmlTableCell c2 = new HtmlTableCell();
    HtmlTableCell c3 = new HtmlTableCell();
    HtmlTableCell c4 = new HtmlTableCell();
    HtmlInputText txtAuthor = new HtmlInputText();
    HtmlInputText txtTitle = new HtmlInputText();
    HtmlInputText txtCategory = new HtmlInputText();
    HtmlInputText txtTarget = new HtmlInputText();
    HtmlInputButton bSave = new HtmlInputButton();
    tblNew.Attributes.Add("Page",pageNumber.ToString());
    bSave.ID = "bSave"+tableNumber;
    HtmlInputButton bPrev = new HtmlInputButton();
    HtmlInputButton bNext = new HtmlInputButton();
    int iprev = tableNumber-1;
    int inext = tableNumber+1;

    bPrev.ID = "bPrev"+iprev;
    bNext.ID = "bNext"+inext;
    bPrev.Value = "^ Prev";
    bNext.Value = "Next v";
    bPrev.Attributes.Add("onclick",
       "doEdit('b"+ iprev+"','"+pageNumber+"')");
    bNext.Attributes.Add("onclick",
       "doEdit('b"+inext +"','"+pageNumber+"')");

    //format
    txtTarget.Size=10;
    tblNew.Style.Add("DISPLAY","none");
    tblNew.ID="#edit"+tableNumber.ToString();
    tblNew.Border=1;
    tblNew.CellSpacing=0;
    tblNew.CellPadding=3;
    tblNew.Width = "520px";
    c1.BgColor="silver";
    c1.Style.Add("FONT-COLOR","WHITE");
    c2.Width="80%";

    c3.Width = "20%";
    //fill cells
    int intProgress = 
        Convert.ToInt32(Double.Parse(Record[4].ToString())/
        Double.Parse(Record[5].ToString())*100);
    if(intProgress>100)
        intProgress=100;
    this.OverallProgress += intProgress;
    txtTarget.Value = Record[5].ToString();
    c2.InnerHtml = "Title: "+Record[1].ToString() + "<br>Sales: $" +Record[4] ;
    c1.InnerHtml = Record[0].ToString();
    c3.Controls.Add(bPrev);
    c3.Controls.Add(bNext);// "Category: " +Record[2].ToString();
    Literal l = new Literal();
    l.Text = "New Target:";

    c4.Controls.Add(l);
    c4.Controls.Add(txtTarget);
    //assign cells to rows
    r1.Cells.Add(c1);
    r1.Cells.Add(c4);
    r2.Cells.Add(c2);
    r2.Cells.Add(c3);
    //assign rows to table
    tblNew.Rows.Add(r1);
    tblNew.Rows.Add(r2);
    return tblNew;
}

BuildTables also calls RenderScript(int Pages, int Items) which asks for the number of pages that will be generated and, new to this iteration, the overall number of items, then injects the appropriate JavaScript into the page. I've added several functions to handle the progress bars and the spinning edit window. The number of items is used to calculate the overall progress.

This method could be broken up into four separate methods--one for each action. I've left them in a single function for simplicity, but future needs may demand a more discrete set of methods.

C#
protected void RenderScript(int Pages, int Items)
{
    int MaxPage = Pages+1;

    StringBuilder s = new StringBuilder("\n<script language="JavaScript">\n");
    //Previous Page
    s.Append("function __onPrevPage ()\n");
    s.Append("{\n");
    s.Append("for (var i=2; i<"+ MaxPage +"; i++) {\n");
    s.Append("if (document.getElementById" + 
             " ('Page' + i).style.display == 'block') {\n");
    s.Append("  document.all('Page' + i).style.display = 'none';\n");
    s.Append("    document.all('Page' +" + 
             " (i - 1)).style.display = 'block';\n");
    s.Append("    break;\n");
    s.Append("}\n");
    s.Append("}\n");
    s.Append("}\n");
    s.Append("\n");
    //Next Page
    s.Append("function __onNextPage ()\n");
    s.Append("{\n");
    s.Append("for (var i=1; i<"+ Pages +"; i++) {\n");
    s.Append(" if (document.getElementById" + 
             " ('Page' + i).style.display == 'block') {\n");
    s.Append("document.all('Page' + i).style.display = 'none';\n");
    s.Append(" document.all('Page' +" + 
             " (i + 1)).style.display = 'block';\n");
    s.Append(" break;\n");
    s.Append(" }\n");
    s.Append(" }\n");
    s.Append(" }\n");

    //First Page
    s.Append("function __onFirstPage ()\n");
    s.Append("{\n");
    s.Append("for (var i=2; i<"+ MaxPage +"; i++) {\n");
    s.Append(" if (document.getElementById" + 
             " ('Page' + i).style.display == 'block') {\n");
    s.Append("document.all('Page' + i).style.display = 'none';\n");
    s.Append(" document.all('Page' + (1)).style.display = 'block';\n");
    s.Append(" break;\n");
    s.Append(" }\n");
    s.Append(" }\n");
    s.Append(" }\n");

    //Jump Page
    s.Append("function __onJumpPage (iPage)\n");
    s.Append("{\n");
    s.Append("for (var i=1; i<"+ MaxPage +"; i++) {\n");
    s.Append(" if (document.getElementById ('Page'" + 
             " + i).style.display == 'block') {\n");
    s.Append("document.getElementById('Page'" + 
             " + i).style.display = 'none';\n");
    s.Append(" document.getElementById('Page'" + 
             "+iPage).style.display = 'block';\n");
    s.Append(" break;\n");
    s.Append(" }\n");
    s.Append(" }\n");
    s.Append(" }\n");

    //Last Page
    s.Append("function __onLastPage ()\n");
    s.Append("{\n");
    s.Append("for (var i=1; i<"+ MaxPage +"; i++) {\n");
    s.Append(" if (document.getElementById" + 
             " ('Page' + i).style.display == 'block') {\n");
    s.Append("document.all('Page' + i).style.display = 'none';\n");
    s.Append(" document.all('Page" + Pages + 
             "').style.display = 'block';\n");
    s.Append(" break;\n");
    s.Append(" }\n");
    s.Append(" }\n");
    s.Append(" }\n");

    //Show overall progress
    s.Append("function __doOverallProgress() \n");
    s.Append("{\n");
    s.Append("document.all('lblProgressOverall').style.width='" + 
                         this.OverallProgress.ToString()+"';\n");
    s.Append("document.all('lblProgressOverallValue').value ='" + 
                                    this.OverallProgress+"';\n");
    s.Append("}\n");

    s.Append("    function doEdit(bval,pval){\n");
    s.Append("    var record = bval.substr(1,bval.length);\n");
    s.Append("    var vTbl = document.getElementById('#vw'+record);\n");
    s.Append("    var eTbl = document.getElementById('#edit'+record);\n");

    s.Append("    vTbl.style.display='none';\n");
    s.Append("    for(var i = 0;i<" + Items +";i++){\n");
    s.Append("        var t = document.getElementById('#edit'+i);\n");
    s.Append("        var n = document.getElementById('#vw'+i);\n");
    s.Append("        if(t.style.display == 'block'){\n");
    s.Append("            t.style.display = 'none';\n");
    s.Append("            n.style.display = 'block';\n");
    s.Append("        }\n");
    s.Append("    }\n");
    s.Append("    eTbl.style.display='block';\n");
    s.Append("    __onJumpPage(eTbl.Page);\n");
    s.Append("    window.location=eTbl.id;\n");
    s.Append("    }\n");

    s.Append("</SCRIPT>");

    Page.RegisterClientScriptBlock("pagingScripts",s.ToString());
}

Next Round

Next, I'm going to clean up the code and create a business object layer to encapsulate the functionality. I'm also going to incorporate my NUnit tests into the code and refactor everything several times over. Finally, I will modify the tables to build on the client from streamed XML that loads in the background.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
Bill Hennessy is a senior .Net architect and Engagement Director with Envision, LLC, a leader in emerging technology solutions, project management, staffing, and design. Mr. Hennessy has over 14 years experience as a software and systems analyst, engineer, and architect.

Mr. Hennessy serves on the Board of Eduction for Bishop DuBourg High School and is a Microsoft Informed Architect.

Mr. Hennessy lives in Wildwood, Missouri, with his wife, Angela, and their five children, Jack, Samantha, Benjamin, Patrick, and Jordan.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Qutbiddin8-Sep-11 0:55
Qutbiddin8-Sep-11 0:55 
GeneralProblem in using the code with Masterpage Pin
Ashu Goel20-Jan-09 23:21
Ashu Goel20-Jan-09 23:21 
GeneralClient Side Sorting on DataGrid. Pin
248912810-Aug-06 4:05
248912810-Aug-06 4:05 
GeneralLooking Out For Client's Best Interest Pin
calexander31-Jan-05 9:43
calexander31-Jan-05 9:43 
GeneralRe: Looking Out For Client's Best Interest Pin
whennessy632-Feb-05 14:28
whennessy632-Feb-05 14:28 
GeneralProblem in FirstPage Pin
kanagav31-Jan-05 3:03
kanagav31-Jan-05 3:03 
GeneralRe: Problem in FirstPage Pin
whennessy632-Feb-05 14:45
whennessy632-Feb-05 14:45 
QuestionHow would this compare with XMLHttpRequest? Pin
csmac314426-Jan-05 9:46
csmac314426-Jan-05 9:46 
AnswerRe: How would this compare with XMLHttpRequest? Pin
whennessy6328-Jan-05 20:44
whennessy6328-Jan-05 20:44 
GeneralWhy have document.all Pin
JCasa19-Jan-05 4:06
JCasa19-Jan-05 4:06 
GeneralRe: Why have document.all Pin
whennessy6319-Jan-05 7:35
whennessy6319-Jan-05 7:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.