ASP.NET DataGrid Paging Part 2 - Custom Paging
By Doug Seven
Published: 1/10/2001
Reader Level: Intermediate Expert
Rated: 4.42 by 12 member(s).
Tell a Friend
Rate this Article
Printable Version
Discuss in the Forums

In last week's tutorial I showed you how to implement the built-in paging functionality using the ASP.NET DataGrid server control (See DataGrid Paging Part 1). In Part 2 of the DataGrid paging tutorials I will show you how to implement a custom paging solution. Downloadable source code includes versions of the code in this tutorial in both Visual Basic .NET and C#. You can download the source code here.

Why Write Paging Code When it is Built-in?

In many cases the built-in paging functionality offered with the DataGrid control will suit your needs. Its advantages include its simplicity in implementation and versatile functionality. However, as in life anything that good has to be hiding something...right. The drawback to the DataGrid's built-in paging functionality is that with each page change a PostBack event fires and the data source is recreated and re-bound to the DataGrid. The entire data source, not just the single page being displayed. If the table you are paging through has one million records in it, all one million records are queried, transported to the application, and then a select few (the current page) are displayed. I'm guessing you can see where you may run into a slight performance issue with this.

With custom paging, you control how many records are returned from the data source. The DataGrid displays rows zero (0) through one less than the PageSize property value of the DataGrid. In other words, if you specify 10 records per page, rows 0-9 (10 records) are bound to the DataGrid, the rest are ignored. Regardless of what page you are rendering, 0 is always the first record bound to the DataGrid.

In this tutorial I will be showing you how to implement a custom paging solution using the ASP.NET DataGrid server control and SQL Server. The page you will be creating will render ten records per page from the Customers table in the Northwind database. The page-navigation links will be "First Page", "Previous Page", "Next Page" and "Last Page". Figure 1.1 shows the final result of what you will learn in this tutorial.

Figure 1.1 - The DataGrid with a Custom Paging Solution

Bring It On - Getting the Data

The heart and soul of a custom paging solution is in how you retrieve the data. You want to retrieve only the data needed for rendering and no more. If your PageSize property is set to 10 (records per page) then you only want to bring back ten records. The trick is getting the correct ten records. For this custom paging solution to work you need the stored procedure in Listing 1.1.

Listing 1.1 - The Stored Procedure

CREATE PROCEDURE [Get_Customers_By_Page]
@CurrentPage int,
@PageSize int,
@TotalRecords int output
AS
--Create a temp table to hold the current page of data
--Add and ID column to count the records

CREATE TABLE #TempTable
(
  ID int IDENTITY PRIMARY KEY,
  CompanyName nvarchar(40),
  ContactName nvarchar (30),
  ContactTitle nvarchar (30),
  Phone nvarchar (24),
  Fax nvarchar (24)
)
--Fill the temp table with the Customers data
INSERT INTO #TempTable
(
  CompanyName,
  ContactName,
  ContactTitle,
  Phone,
  Fax
)
SELECT
  CompanyName,
  ContactName,
  ContactTitle,
  Phone,
  Fax
FROM
  Customers
--Create variable to identify the first and last record that should be selected
DECLARE @FirstRec int, @LastRec int
SELECT @FirstRec = (@CurrentPage - 1) * @PageSize
SELECT @LastRec = (@CurrentPage * @PageSize + 1)
--Select one page of data based on the record numbers above
SELECT
  CompanyName,
  ContactName,
  ContactTitle,
  Phone,
  Fax
FROM
  #TempTable
WHERE
  ID > @FirstRec
AND
  ID < @LastRec
--Return the total number of records available as an output parameter
SELECT @TotalRecords = COUNT(*) FROM Customers

Break It Down

In Listing 1.1 you create a stored procedure to return only the records you need. This is done by passing in two parameters, the page number being requested (@CurrentPage) and the number of records per page (@PageSize). Additionally you create an output parameter to return the total number of records in the Customers table.

The stored procedure creates a temporary table and adds an Identity column (ID). The ID column is used to find the correct records to return. Once you fill the temporary table with only data from the columns you want to return, then you can select the specific rows based on the @CurrentPage value and the @PageSize value. This is done by mathematically determining the first record (@FirstRec) and the last record (@LastRec) to return. Lastly you get the value for @TotalRecs by using the COUNT() aggregate function.

I've Got the Data, Now What?

Now that you have the stored procedure you can get to work building the ASP.NET code to retrieve the correct data. While the sample code in this article is in Visual Basic.NET, the downloadable sample code has both VB and C# code.

Allow me to start at the end by showing you the page code first, then the programmatic code. Listing 1.2 shows the Web Form page code.

Listing 1.2 - The Web Form Page Code

<%@ Page Inherits="GridCustomPaging" Src="2001010902.aspx.cs" %>
<html>
<head>
 <style ref="stylesheet" type="text/css">
  .tableItem {font: x-small Verdana, Arial, sans-serif;}
  .tableHeader {font: bold small Arial; color:#663300; background-color:#CCCC66;}
  .alternatingItem {font: x-small Verdana, Arial, sans-serif; background-color:#FFFFCC;}
  A {color:#663300}
  A:hover {color:red}
  .pageLinks {font: bold x-small Verdana, Arial, sans-serif;}
 </style>
</head>
<body>
<form runat="server" method="post">
 <asp:DataGrid runat="server" id="MyDataGrid"
  Width="740" CellPadding="4" CellSpacing="0"
  Gridlines="Horizontal" HorizontalAlign="Center"

  HeaderStyle-CssClass="tableHeader"
  ItemStyle-CssClass="tableItem"
  AlternatingItemStyle-BackColor="#FFFFCC"

  AllowPaging="True"
  AllowCustomPaging="True"
  PageSize="10"
  PagerStyle-Visible="False"
 />
 <center>
 <p class="pageLinks">
  <b>Page
  <asp:Label id="CurrentPage" CssClass="pageLinks" runat="server" />
  of
  <asp:Label id="TotalPages" CssClass="pageLinks" runat="server" />
 </p>
 <asp:LinkButton runat="server" CssClass="pageLinks"
  id="FirstPage" Text="[First Page]"
  OnCommand="NavigationLink_Click" CommandName="First" />
 <asp:LinkButton runat="server" CssClass="pageLinks"
  id="PreviousPage" Text="[Previous Page]"
  OnCommand="NavigationLink_Click" CommandName="Prev" />
 <asp:LinkButton runat="server" CssClass="pageLinks"
  id="NextPage" Text="[Next Page]"
  OnCommand="NavigationLink_Click" CommandName="Next" />
 <asp:LinkButton runat="server" CssClass="pageLinks"
  id="LastPage" Text="[Last Page]"
  OnCommand="NavigationLink_Click" CommandName="Last" />
 </center>
</form>
</body>
</html>

In Listing 1.2 you place a DataGrid on a Web Form. To enable paging you must set the AllowPaging property to True. For custom paging you want to disable the built-in paging functions, but still leave PageSize active. Setting AllowCustomPaging to True indicated to the the DataGrid that you will be paging data according to the PageSize property, but that the built-in paging will not be used. Also, you do not want the DataGrid's page-navigation links to render since you will be providing your own controls for paging. Setting the PagerStyle-Visible property to False hides the page-navigation links.

Just under the DataGrid are two Label controls. These are used to indicate to the user where they are (Page 7 of 700 for example). Below that are the LinkButton controls you will use to navigate through the data. Each LinkButton specifies an event handler named NavigationLink_Click() as its OnCommand event handler. The same event handler will be used for all four LinkButtons. Their CommandName property will be used to determine which LinkButton was clicked, and how to navigate through the pages.

The Code Behind

To handle the sequence of events that occurs when a page is requested, then subsequent pages are navigated to you need to create two event handlers and one method - Page_Load(), NavigationLink_Click() and BindData(). The Page_Load() event handler will be used to set values only when the page request is not a post-back. Because a click on a LinkButton triggers a PostBack event we will handle post-backs separately. The NavigationLink_Click() event will handle post-backs when a page-navigation link is clicked, and set values for retrieving the data. The BindData() method is the guts of the whole operation. This is where you will call to the stored procedure and return only the data you need.

Listing 1.3 shows the begining of the code behind class.

Listing 1.3 - The Page_Load() Event Handler in the Code Behind Class

using System;
using System.Data;
using System.Data.SqlClient;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;

public class GridCustomPaging : System.Web.UI.Page
{
  protected System.Web.UI.WebControls.DataGrid MyDataGrid;
  protected System.Web.UI.WebControls.Label CurrentPage;
  protected System.Web.UI.WebControls.Label TotalPages;
  protected System.Web.UI.WebControls.LinkButton NextPage;
  protected System.Web.UI.WebControls.LinkButton PreviousPage;
  protected System.Web.UI.WebControls.LinkButton FirstPage;
  protected System.Web.UI.WebControls.LinkButton LastPage;

  protected Int32 _currentPageNumber = 1;

  public void Page_Load ( Object sender, EventArgs e )
  {
    if ( !Page.IsPostBack )
    {
      BindData();
    }
  }

Before you create the Page_Load() event handler, you need to create a class-level variable for the page number being requested. This is the _currentPageNumber variable. It will be used in both event handlers and the BindData() method. By default we set it to 1.

The Page_Load() event handler calls BindData() only when the current page request is not a PostBack event. On the first call to the page the _currentPageNumber is set to 1, to render the first page of data. Subsequent page requests will be handled by the NavigationLink_Click() event handler.

Listing 1.4 - The NavigationLink_Click() Event Handler

  protected void NavigationLink_Click ( Object sender, CommandEventArgs e )
  {
    switch ( e.CommandName )
    {
      case "First":
        _currentPageNumber = 1;
        break;
      case "Last":
        _currentPageNumber = Int32.Parse ( TotalPages.Text );
        break;
      case "Next":
        _currentPageNumber = Int32.Parse ( CurrentPage.Text ) + 1;
        break;
      case "Prev":
        _currentPageNumber = Int32.Parse ( CurrentPage.Text ) - 1;
        break;
    }

    BindData();
  }

In the NavigationLink_Click() event handler you simply reset the _currentPageNumber value depending on the LinkButton that was clicked. A switch (or Select Case in Visual Basic .NET) evaluator is perfect for this purpose. Since both the page number and the total number of records are being displayed in Label controls on the page, you can use their value (maintained state with ViewState) to base the new _currentPageNumber value from. After the _currentPage value is set, call the BindData() method.

Listing 1.5 - The BindData() Method

  public void BindData()
  {
    SqlConnection myConnection = new SqlConnection(
      "server=localhost;database=northwind;trusted_connection=true;" );

    SqlCommand myCommand = new
      SqlCommand ( "Get_Customers_By_Page", myConnection );
    myCommand.CommandType = CommandType.StoredProcedure;

    myCommand.Parameters.Add ( new
      SqlParameter ( "@CurrentPage", SqlDbType.Int ) ).Value = _currentPageNumber;
    myCommand.Parameters.Add ( new
      SqlParameter ( "@PageSize", SqlDbType.Int ) ) .Value = MyDataGrid.PageSize;
    myCommand.Parameters.Add ( new
      SqlParameter ("@TotalRecords",
        SqlDbType.Int ) ).Direction = ParameterDirection.Output;

    try
    {
      myConnection.Open();
      MyDataGrid.DataSource = myCommand.ExecuteReader();
      MyDataGrid.DataBind();
    }
    finally
    {
      myConnection.Close();
    }

    CurrentPage.Text = _currentPageNumber.ToString();

    Double _totalPages = 1;
    if ( !Page.IsPostBack )
    {
      Int32 _totalRecords = (Int32) myCommand.Parameters["@TotalRecords"].Value;
      _totalPages = _totalRecords / MyDataGrid.PageSize;
      TotalPages.Text = ( System.Math.Ceiling ( _totalPages ) ).ToString();
    }
    else
    {
      _totalPages = Double.Parse ( TotalPages.Text );
    }

    if ( _currentPageNumber == 1 )
    {
      PreviousPage.Enabled = false;

      if ( _totalPages > 1 )
      {
        NextPage.Enabled = true;
      }
      else
      {
        NextPage.Enabled = false;
      }
    }
    else
    {
      PreviousPage.Enabled = true;

      if ( _currentPageNumber == _totalPages )
      {
        NextPage.Enabled = false;
      }
      else
      {
        NextPage.Enabled = true;
      }
    }
  }

The BindData() method uses a SqlCommand to get the data fro the database. You must set the myCommand.CommandType = CommandType.StoredProcedure to indicate the first parameter passed into the SqlCommand is the name of a stored procedure, not a SQL statement.

The @CurrentPage parameter is the number of the page being requested (the CurrentPageNumber value). The @PageSize parameter is the value of the DataGrid's PageSize property. Since the @TotalRecs parameter is an output parameter you specify this with the myCommand.Parameters["@TotalRecords"].Direction = ParameterDirection.Output declaration. When you execute the command only the number of records you need are returned. When you bind the DataGrid, the number of records specified in the PageSize property are bound.

Note: In Visual Basic .NET you can use the With keyword to set all of the parameters of the stored procedure. In C# the With keyword is not available.

You use an if{...} evaluator to check if this is the first request for the page. For this example I am not worried that the number of records in the table may change between requests, so I am having you set the TotalRecords.Text value only on the first request. Its state will be maintained across multiple requests. To do this you use the Ceiling method of the System.Math class. Since you returned the total number of records from the Customers table, not the total number of pages you must do a little math to get the total number of pages. I chose to do this so you have the total number of records available if you need it. We could easily move the math to the stored procedure and return the total number of pages. A point to note is that if you are using custom paging and the DataGrid's standard navigation links (PagerStyle.Visible = true), then you need to set the VirtualItemCount of the DataGrid to the total number of records. This MUST be done at run-time; it is not allowed to be set at design-time.

The Ceiling method takes in a Double datatype and returns the nearest whole number equal to or greater than the values passed in. For example, if the total number of records is 91 and the PageSize is 10, simple division returns 9.1 (9 pages of 10 records and one additional record). What you really want is 10 pages, with the last page only showing one record. Ceiling takes the result of this division and returns 10, the nearest whole number equal to or greater than 9.1.

Using another if{...} evaluator you either enable or disable the "Previous Page" and "Next Page" LinkButtons based on the _currentPageNumber value. You certainly do not want a "Previous Page" link if the user is looking at the first page. The Enable property of the LinkButton determines whether or not the Text property renders with an HREF attribute or not. Either way the text is still displayed.

Yeah, But Does It Work

Now all the code is in place. Browse to the Web Form to see the fruits of your labor. You have implemented a custom paging solution that keeps resource usage to a minimum by retrieving only the values need to be rendered.

Next week I will continue on with the DataGrid by showing you how to implement column sorting.



Marketplace
(Sponsored Links)
What are the green links?
   



 
Copyright © 2007 CMP Tech LLC |
Privacy Policy (4/10/06) | Your California Privacy Rights (4/10/06) | Terms of Service | Advertising Info | About Us | Help