Creating a Pager Control for ASP.NET

 

Dino Esposito
Wintellect

October 2003

Applies to:
   Microsoft® ASP.NET

Summary: Tackles the problem of adding paging capabilities to any ASP.NET control. Also gives a number of tips and tricks useful when developing composite ASP.NET controls. (18 printed pages)

Download the source code for this article.

From the programmer's perspective, one of the worst drawbacks of Microsoft® SQL Server™ queries is that they often return much more rows than actually fit in the application's user interface. This unfortunate circumstance puts developers in a dilemma. Should they create a very long page that takes users a while to scroll through, or is the issue better addressed by setting up a manual paging mechanism?

Which solution is better depends mostly upon the nature of the data you retrieve. A long list of items—for example, the result of a search—is preferably rendered through equally sized, relatively short pages. A long, single item, like the text of an article, is more comfortably consumed if inserted entirely in the page. In the final analysis, the decision should be made in light of the overall usability of the application. So, how does Microsoft® ASP.NET face the problem of data paging?

ASP.NET provides powerful data-bound controls to format the results of a query into HTML markup. However, only one of these data-bound controls—specifically, the DataGrid control—natively supports paging. Other controls, such as the DataList, the Repeater, or the CheckBoxList, just don't page. These and other list controls don't page, not because they are structurally unable to page, but because they, unlike the DataGrid, don't contain any specific code that handles paging. However, the code to handle paging is relatively boilerplate and can be added to any of these controls.

Scott Mitchell covered DataGrid paging in a recent article titled Creating a Pageable, Sortable DataGrid. The article also references other useful information around the Web that gives you the basics, and more, relating to grid paging. If you want to see an example of how to make the DataList control page, have a look at this article. It demonstrates how to create a custom DataList control that features current index and page-size properties and fires a page-changed event.

The same code can be reworked to serve the paging needs of other list controls, such as the ListBox and the CheckBoxList. Nevertheless, adding paging capabilities to individual controls is not really all that great of an idea, for as mentioned, the paging code is rather boilerplate. So what could be better for a smart programmer, than stuffing it all into a new general-purpose pager control?

In this article, I'll build a pager control that will make a buddy list control page over the results of a SQL Server query. The control is named SqlPager and supports two types of buddy controls—list controls and base data list controls.

Highlights of the SqlPager Control

The SqlPager control is an ASP.NET composite control that contains a one-row table. The row, in turn, contains two cells—the navigation bar and the page descriptor. The user interface of the control is a strip that ideally is the same width as the buddy control. The navigation bar section provides the clickable elements to move through pages; the page descriptor section gives users some feedback about the currently displayed page.

ms972960.pagercontrols_fig01(en-us,MSDN.10).gif

Figure 1. The SqlPager control as it shows up in the Visual Studio .NET page designer

Just like the embedded pager of the DataGrid control, the SqlPager control has two navigation modes—next/previous and numeric pages. An ad hoc property, PagerStyle, lets you choose the more convenient style. The control works in conjunction with a list control. You assign the pager such a buddy control through the ControlToPaginate string property.

SqlPager1.ControlToPaginate = "ListBox1"; 

Basically, the pager gets the results of a SQL Server query, prepares an appropriate page of records, and displays that through the DataSource property of the buddy control. When the user clicks to view a new page, the pager retrieves the requested data and shows it again through the buddy. The paging mechanism is completely transparent to the list control. The data source of the list control is programmatically updated and contains, at any time, only the records that fit into the current page.

The paging engine of the control features quite a few public properties, such as CurrentPageIndex, ItemsPerPage, and PageCount to get and set the index of the current page, the size of each page, and the total number of displayed pages. The pager manages any logic needed for data retrieval and paging.

The SelectCommand property sets the text of the command to use for fetching data. The ConnectionString property defines name and location of the database, plus the credentials to connect. How the query is executed depends on the value of the PagingMode property. Feasible values for the property are the values of the homonym PagingMode enumeration—Cached and NonCached. If the Cached option is selected, the entire result set is retrieved using a data adapter and a DataTable object. The result set is optionally placed in the ASP.NET Cache object and reused until it expires. If the NonCached option is selected, the query retrieves only the records that fit into the current page. No data is placed in the ASP.NET Cache this time. The NonCached mode is nearly identical to the custom paging mode of the DataGrid control.

The full programming interface of the SqlPager control is shown in the table below.

Table 1. The programming interface of the SqlPager control

Name Type Description
CacheDuration Property Indicates how many seconds the data should stay in the ASP.NET cache. Used only in Cached mode. Default is 60 seconds.
ConnectionString Property The connection string to access the SQL Server database of choice.
ControlToPaginate Property The ID of the control in the same .aspx page that will show the pages of records retrieved by the pager. This is the buddy control.
CurrentPageIndex Property Gets and sets the 0-based index of the page
ItemsPerPage Property Gets and sets the number of records to display per page. Default is 10 items per page.
PagerStyle Property A value that indicates the style of pager's user interface. It can be one of the values in the PagerStyle enum: NextPrev and NumericPages. In NextPrev mode, VCR-like buttons are displayed to reach the first, previous, next, and last page. In the NumericPages mode, a drop-down list is filled with the indexes of all available pages.
PagingMode Property A value that indicates how data is retrieved. It can be one of the values in the PagingMode enum: Cached and NonCached. If it is Cached, a data adapter is used and the whole result set is parked in the ASP.NET cache. If it is NonCached, only the records in the current page are retrieved. No caching takes place in this case.
SelectCommand Property The command text of the query. It is expected to be in the form SELECT-FROM-WHERE. The ORDER BY clause is not supported. Sorting is otherwise provided through the SortField property.
SortField Property The name of the field to sort by. This field is used to prepare a dynamic ORDER BY clause for the query. Sorting is performed by SQL Server.
ClearCache Method Removes any data stored in the ASP.NET cache.
PageIndexChanged Event Default event, occurs when the pager moves to a new page. The event data structure is the PageChangedEventArgs class and contains the index of the old and new page.

Since the SqlPager control inherits WebControl, it also features a bunch of UI-related properties to manage fonts, borders, and colors.

Building the SqlPager Control

I'll build the SqlPager control as a composite control and have it inherit the WebControl class. Composite controls are a special flavor of ASP.NET server controls that result from the combination of one or more constituent server controls.

public class SqlPager : WebControl, INamingContainer
{ ... }

Unless you are building a completely custom control or extending an existing one, most of the time when you create a new control, you are actually building a composite control. To create the SqlPager I will assemble a Table control and either a few LinkButton controls or a DropDownList control, depending on the pager style.

There are a few guidelines to bear in mind to build composite controls. First off, you have to override the CreateChildControls protected method. The CreateChildControls method is inherited from Control and is called when server controls have to create child controls for rendering or after a postback.

protected override void CreateChildControls()
{
   // Clear existing child controls and their viewstate
   Controls.Clear();
   ClearChildViewState();

   // Build the control tree
   BuildControlHierarchy();
}

When overriding this method, you should do a couple of important things. You create and initialize any required instance of the child controls and add them to the Controls collection of the parent control. Before you go with the new control tree, though, you should remove any existing child controls and clear any viewstate information child controls may have left around.

A composite component should also implement the INamingContainer interface, so that the ASP.NET runtime can create a new naming scope for it. This ensures that all controls in the composite control have a unique name. This will also ensure that the postback data of child controls is handled automatically.

Being a naming container is particularly important for the SqlPager control. SqlPager, in fact, contains some LinkButton controls and needs to catch and handle their click events in order to navigate pages. Just like any other control in an ASP.NET page, a LinkButton is given an ID, which is used to identify the control that will handle the postback event.

When handling a postback, the ASP.NET runtime attempts to find a match between the event target ID and the ID of any control that is a direct child of the main form. Our LinkButton is a child of the pager and is subsequently unable to run its server-side code. Does this mean that only direct children of the form can fire and handle server events? Of course not, as long as you use naming containers.

By making the SqlPager control implement the INamingContainer interface, you change the actual ID of the embedded link button from, say, First to SqlPager1:First. When users click to view a new page, the postback event has SqlPager1:First as the target control. The algorithm that the runtime uses to identify the target control is actually a bit more sophisticated than I described a moment ago. The runtime considers the name of the event target as a colon-separated string. The match is actually sought between the children of the form and the first token of a colon-separated string, such as SqlPager1:First. Since the pager is a child of the form, the match is found and the pager gets the click event. If you find this explanation inadequate or confusing, just download the source code of the SqlPager control, remove the INamingContainer marker interface, and recompile. You'll see that the pager posts back but is unable to handle the click event internally.

The INamingContainer interface is a method-less, marker interface whose implementation doesn't require you to do more than to specify the name in the class declaration.

Another important aspect of composite controls is that they normally don't require custom logic for rendering. The rendering of a composite control follows from the rendering of constituent controls. When building a composite control, you normally don't have to override the Render method.

The SqlPager tree of controls consists of a table with one row and two cells. The table inherits most of the visual settings of the pager—foreground and background colors, borders, font information, and width. The first cell contains the navigation bar whose structure depends on the value of the PagerStyle property. If the pager style is NextPrev, the navigation bar is made of four VCR-like link buttons. Otherwise, it will consist of a drop-down list.

private void BuildControlHierarchy()
{
   // Build the surrounding table (one row, two cells)
   Table t = new Table();
   t.Font.Name = this.Font.Name;
   t.Font.Size = this.Font.Size;
   t.BorderStyle = this.BorderStyle;
   t.BorderWidth = this.BorderWidth;
   t.BorderColor = this.BorderColor;
   t.Width = this.Width;
   t.Height = this.Height;
   t.BackColor = this.BackColor;
   t.ForeColor = this.ForeColor;

   // Build the table row
   TableRow row = new TableRow();
   t.Rows.Add(row);

   // Build the cell with the navigation bar
   TableCell cellNavBar = new TableCell();
   if (PagerStyle == this.PagerStyle.NextPrev)
      BuildNextPrevUI(cellNavBar);
   else
      BuildNumericPagesUI(cellNavBar);
   row.Cells.Add(cellNavBar);

   // Build the cell with the page index
   TableCell cellPageDesc = new TableCell();
   cellPageDesc.HorizontalAlign = HorizontalAlign.Right;
   BuildCurrentPage(cellPageDesc);
   row.Cells.Add(cellPageDesc);

   // Add the table to the control tree
   this.Controls.Add(t);
}

It is extremely important for the correct rendering of the pager that you add each control to the proper Controls collection. The outermost table must be added to the Controls collection of the pager. The link buttons and the drop-down list must be added to the Controls collection of the respective table cell.

The following code gives an idea of the code used to build the link buttons navigation bar. Each button is rendered with a Webdings character, disabled as needed, and bound to an internal Click event handler.

private void BuildNextPrevUI(TableCell cell)
{
   bool isValidPage = ((CurrentPageIndex >=0) && 
                      (CurrentPageIndex <= TotalPages-1));
   bool canMoveBack = (CurrentPageIndex>0);
   bool canMoveForward = (CurrentPageIndex<TotalPages-1);

   // Render the << button
   LinkButton first = new LinkButton();
   first.ID = "First";
   first.Click += new EventHandler(first_Click);
   first.Font.Name = "webdings";
   first.Font.Size = FontUnit.Medium;
   first.ForeColor = ForeColor;
   first.ToolTip = "First page";
   first.Text = "7";   
   first.Enabled = isValidPage && canMoveBack;
   cell.Controls.Add(first);
   :
}

The alternative style for the pager—numeric pages listed in a drop-down list—is built as follows:

private void BuildNumericPagesUI(TableCell cell)
{
   // Render a drop-down list  
   DropDownList pageList = new DropDownList();
   pageList.ID = "PageList";
   pageList.AutoPostBack = true;
   pageList.SelectedIndexChanged += new EventHandler(PageList_Click);
   pageList.Font.Name = this.Font.Name;
   pageList.Font.Size = Font.Size;
   pageList.ForeColor = ForeColor;
   
   if (TotalPages <=0 || CurrentPageIndex == -1)
   {
      pageList.Items.Add("No pages");
      pageList.Enabled = false;
      pageList.SelectedIndex = 0; 
   }
   else // Populate the list
   {
      for(int i=1; i<=TotalPages; i++)
      {
         ListItem item = new ListItem(i.ToString(), (i-1).ToString());
         pageList.Items.Add(item);
      }
      pageList.SelectedIndex = CurrentPageIndex;
   }
}

All event handlers—Click and SelectedIndexChanged—end up changing the currently displayed page. Both methods fall into a common GoToPage private method.

private void first_Click(object sender, EventArgs e)
{
   GoToPage(0);
}
private void PageList_Click(object sender, EventArgs e)
{
   DropDownList pageList = (DropDownList) sender;
   int pageIndex = Convert.ToInt32(pageList.SelectedItem.Value);
   GoToPage(pageIndex);
}
private void GoToPage(int pageIndex)
{
   // Prepares event data
   PageChangedEventArgs e = new PageChangedEventArgs();
   e.OldPageIndex = CurrentPageIndex;
   e.NewPageIndex = pageIndex;

   // Updates the current index
   CurrentPageIndex = pageIndex;

   // Fires the page changed event
   OnPageIndexChanged(e);

   // Binds new data
   DataBind();
}

Handlers for other navigation buttons differ from first_Click only for the page number they pass to the GoToPage method. The GoToPage method is responsible for the PageIndexChanged event and for firing the data-binding process. It prepares the event data (old and new page index) and triggers the event. The GoToPage is defined as private but you can programmatically change the displayed page using the CurrentPageIndex property.

public int CurrentPageIndex
{
   get {return Convert.ToInt32(ViewState["CurrentPageIndex"]);}
   set {ViewState["CurrentPageIndex"] = value;}
}

CurrentPageIndex property, like all the properties listed in Table 1, has a pretty simple implementation. It saves its content to, and restore it from, the viewstate. The page index is validated and used during the data binding step.

The Data Binding Step

The DataBind method is common to all ASP.NET controls and, for data-bound controls, it triggers the refresh of the user interface to reflect new data. The SqlPager control uses this method to start the data retrieval operation based on the values of the SelectCommand and ConnectionString properties. It goes without saying that the process aborts if any of these properties are blank. Likewise, the data binding step is canceled if the buddy control doesn't exist. To locate the buddy control, the DataBind method uses the FindControl function on the Page class. It stems from this that the buddy control must be a direct child of the main form.

The control to paginate can't be an arbitrary ASP.NET server control. It must be either a list control or a base data list. More generally, the buddy control must expose the DataSource property and implement the DataBind method. These are actually the only requirements for a potential pageable control. All controls in the Microsoft® .NET Framework that inherit from either ListControl or BaseDataList fulfill the first requirement; all Web controls, instead, by design meet the DataBind requirement. With the current implementation, you can't use the SqlPager control to page a Repeater. Unlike its companion controls, DataList and DataGrid, the Repeater doesn't inherit BaseDataList, nor does it provide the features of a list control. The table below lists the controls you can page with SqlPager.

Table 2. Data-bound controls that can be paged by the SqlPager control

Control Description
CheckBoxList Derived from ListControl, renders as a list of check boxes.
DropDownList Derived from ListControl, renders as a drop-down list of strings.
ListBox Derived from ListControl, renders a scrollable list of strings.
RadioButtonList Derived from ListControl, renders as a list of radio buttons.
DataList Derived from BaseDataList, represents a list of templated data items.
DataGrid Derived from BaseDataList, represents a tabular grid of data items. The DataGrid is the only ASP.NET control to embed a rich paging engine.

The code below illustrates the data binding process as implemented by the SqlPager control.

public override void DataBind()
{
   // Fires the data binding event
   base.DataBinding();

   // Controls must be recreated after data binding 
   ChildControlsCreated = false;

   // Ensures the control exists and is a list control
   _controlToPaginate = Page.FindControl(ControlToPaginate);
   if (_controlToPaginate == null)
      return;
   if (!(_controlToPaginate is BaseDataList || 
         _controlToPaginate is ListControl))
      return;

   // Ensures enough info to connect and query is specified
   if (ConnectionString == "" || SelectCommand == "")
      return;

   // Fetch data
   if (PagingMode == PagingMode.Cached)
      FetchAllData();
   else
      FetchPageData();

   // Bind data to the buddy control
   BaseDataList baseDataListControl = null;
   ListControl listControl = null;
   if (_controlToPaginate is BaseDataList)
   {
       baseDataListControl = (BaseDataList) _controlToPaginate;
       baseDataListControl.DataSource = _dataSource; 
       baseDataListControl.DataBind();
       return;
   }
   if (_controlToPaginate is ListControl)
   {
       listControl = (ListControl) _controlToPaginate;
       listControl.Items.Clear(); 
       listControl.DataSource = _dataSource; 
       listControl.DataBind();
       return;
   }
}

A different fetch routine is called according to the value of the PagingMode property. In any case, the resultset is bound to an instance of the PagedDataSource class. This class provides some facilities to page data. In particular, when the whole data set is cached, the class automatically retrieves the records for the current page and returns boolean values to give information about the first and the last page. I'll come back to the internal structure of this class shortly. In the listing above, the helper PagedDataSource object is represented by the _dataSource variable.

Next, the SqlPager control figures out the type of the buddy control and binds the contents of the PagedDataSource object to the buddy's DataSource property.

At a certain point, the DataBind method shown above also resets the ChildControlsCreated property to false. Why is it so?

When the page that contains the pager posts back, all controls are recreated; the pager is no exception. Normally, all controls and their children are created by the time the page is ready for rendering. A moment before each control receives the OnPreRender notification, the protected EnsureChildControls method is called so that each control can build its control tree. When this happens, the data binding process is complete and the new data has been cached.

However, when the page posts back because one of the pager constituent controls is clicked (that is, a user clicked to change the page), the control tree of the pager is built, long before the rendering stage. In particular, the tree must be in place when the related server-side event is processed, and consequently, before the data binding starts. The rub is that the data binding modifies the page index and this must be reflected in the user interface. If you don't take countermeasures, the page index in the pager won't be refreshed when users switch to another page.

There are various ways to fix this, but it's important to be aware of the issue and its real causes. You can avoid building a control tree and generate all the output in the Render method. Alternatively, you can modify the portions of the tree affected by the data binding changes. I've chosen a third way, one that requires less code and works whatever portions of the control's user interface are affected by the data binding changes. By setting the ChildControlsCreated property to false, you invalidate any previously created tree of controls. As a result, the tree will be recreated prior to rendering.

The Paging Engine

The SqlPager control supports two ways of retrieving data—cached and non-cached. In the former case, the select command executes as is and the whole resultset is bound to the internal paged data source object. The PagedDataSource object will automatically return the records that fit into a particular page. The PagedDataSource class is also the system component working behind the DataGrid default paging mechanism.

Retrieving all the records to display only the few that fit into a page is not always a smart approach. Due to the stateless nature of Web applications, in fact, the potentially large query runs every time the page is requested. To be effective, the cached approach must rely on some sort of cache object and the ASP.NET Cache object is an excellent candidate. The use of caching techniques make the application run faster, but also offers a snapshot of the data that doesn't reflect the most recent changes. In addition, more memory is used on the Web server. Paradoxically, this might even create a scalability problem if large a amount of data is cached on a per-session basis. The Cache container is global to the application; if the data is stored in it on a per-session basis, you also need to generate session-specific entry names.

On the up side of the Cache object is full support for expiration policies. In other words, the data stored in the cache can be automatically released after a certain duration. The following code illustrates a private method of the SqlPager class that fetches data and stores it in the cache.

private void FetchAllData()
{
   // Looks for data in the ASP.NET Cache
   DataTable data;
   data = (DataTable) Page.Cache[CacheKeyName];
   if (data == null)
   {
      // Fix SelectCommand with order-by info
      AdjustSelectCommand(true);

      // If data expired or has never been fetched, go to the database
      SqlDataAdapter adapter = new SqlDataAdapter(SelectCommand, 
            ConnectionString);
      data = new DataTable();
      adapter.Fill(data);
      Page.Cache.Insert(CacheKeyName, data, null, 
           DateTime.Now.AddSeconds(CacheDuration), 
           System.Web.Caching.Cache.NoSlidingExpiration);
   }
      
   // Configures the paged data source component
   if (_dataSource == null)
      _dataSource = new PagedDataSource(); 
   _dataSource.DataSource = data.DefaultView;  
   _dataSource.AllowPaging = true;
   _dataSource.PageSize = ItemsPerPage;
   TotalPages = _dataSource.PageCount; 

   // Ensures the page index is valid 
   ValidatePageIndex();
   if (CurrentPageIndex == -1)
   {
      _dataSource = null;
      return;
   }

   // Selects the page to view
   _dataSource.CurrentPageIndex = CurrentPageIndex;
}

The name of the cache entry is unique to the control and request. It includes the URL of the page and the ID of the control. The data is bound to the cache for the specified number of seconds. To give items an expiration, you must use the Cache.Insert method. The following, and simpler, code will add the item to the cache but doesn't include any expiration policy.

Page.Cache[CacheKeyName] = data;

The PagedDataSource object gets the data to page through its DataSource property. It is worth noting that the DataSource property of the PagedDataSource class accepts only IEnumerable objects. The DataTable doesn't meet this requirement; that's why I resort to the DefaultView property.

The SelectCommand property determines the query run against the SQL Server database. This string is expected to be in the form SELECT-FROM-WHERE. No ORDER BY clause is supported and, if specified, is stripped off. This is just what the AdjustSelectCommand method does. Any sorting information can be specified using the SortField property. The AdjustSelectCommand method itself adds a proper ORDER BY clause based on the value of SortField. Is there a reason for this?

When the pager works in NonCached mode, the original query is modified to ensure that only the records for the current page are retrieved. The real query text that hits SQL Server takes the following form.

SELECT * FROM 
(SELECT TOP ItemsPerPage * FROM 
(SELECT TOP ItemsPerPage*CurrentPageIndex * FROM 
(SelectCommand) AS t0 
ORDER BY SortField ASC) AS t1
ORDER BY SortField DESC) AS t2 
ORDER BY SortField

The query makes up for the lack of a ROWNUM clause in SQL Server 2000 and reorders records in such a way that only the n.th block of x items is returned properly sorted. You specify the base query and the pager breaks it up into smaller pages. Only the records that fit into a page are returned. As you can see, the query above needs to handle the sort field aside from the query. That's why I added the SortField as a separate property. The only drawback of this code is that it defaults to ascending order. By making the ASC/DESC keywords parametric, you can make this code virtually perfect:

private void FetchPageData()
{
   // Need a validated page index to fetch data.
   // Also need the virtual page count to validate the page index
   AdjustSelectCommand(false);
   VirtualRecordCount countInfo = CalculateVirtualRecordCount();
   TotalPages = countInfo.PageCount;

   // Validate the page number (ensures CurrentPageIndex is valid or -1)
   ValidatePageIndex();
   if (CurrentPageIndex == -1)
      return;

   // Prepare and run the command
   SqlCommand cmd = PrepareCommand(countInfo);
   if (cmd == null)
      return;
   SqlDataAdapter adapter = new SqlDataAdapter(cmd);
   DataTable data = new DataTable();
   adapter.Fill(data);

   // Configures the paged data source component
   if (_dataSource == null)
      _dataSource = new PagedDataSource(); 
   _dataSource.AllowCustomPaging = true;
   _dataSource.AllowPaging = true;
   _dataSource.CurrentPageIndex = 0;
   _dataSource.PageSize = ItemsPerPage;
   _dataSource.VirtualCount = countInfo.RecordCount;
   _dataSource.DataSource = data.DefaultView;   
}

In NonCached mode, the PagedDataSource object doesn't hold the whole data source and can't figure out the total number of pages to page through. For this reason, you have to flag the AllowCustomPaging property and provide a virtual count of the records in the data source. The virtual count is normally retrieved using a SELECT COUNT(*) query. This model is nearly identical to DataGrid custom paging. Finally, the current page index to select in the PagedDataSource object is always 0, because one page of records is actually stored.

So much for the implementation of the SqlPager control, let's have a look at how you would use it.

Working with the SqlPager Control

Let's consider a sample page that contains a ListBox control. To use the pager, make sure the .aspx page properly registers the control's assembly.

<%@ Register TagPrefix="expo" Namespace="DevCenter" Assembly="SqlPager" %>

The control's markup depends on the properties actually set. The following markup is a reasonable example:

<asp:listbox runat="server" id="ListBox1" 
     Width="300px" Height="168px"
     DataTextField="companyname" /> 
<br>
<expo:SqlPager runat="server" id="SqlPager1" Width="300px" 
   ControlToPaginate="ListBox1" 
   SelectCommand="SELECT customerid, companyname FROM customers" 
      ConnectionString="SERVER=localhost;DATABASE=northwind;UID=..."
      SortKeyField="companyname" />
<br>
<asp:button runat="server" id="LoadFirst1" Text="Load first page" /> 

Besides the pager, the page contains a listbox and a button. The listbox will show the contents of each page; the button simply serves to populate the listbox for the first time. The button has a click event handler defined as follows.

private void LoadFirst1_Click(object sender, EventArgs e) {
   SqlPager1.CurrentPageIndex = 0;
   SqlPager1.DataBind(); 
}

Figure 2 shows the page in action.

ms972960.pagercontrols_fig02(en-us,MSDN.10).gif

Figure 2. The SqlPager control works with a ListBox control.

An even more interesting example can be built using a DataList control. The idea is to use the pager to navigate through the personal record of each Northwind employee. The DataList looks like in the following listing.

<asp:datalist runat="server" id="DataList1" Width="300px" 
     Font-Names="Verdana" Font-Size="8pt">
<ItemTemplate>
<table bgcolor="#f0f0f0" style="font-family:verdana;font-size:8pt;">
  <tr><td valign="top">
      <b><%# DataBinder.Eval(Container.DataItem, "LastName") + ", " + 
      DataBinder.Eval(Container.DataItem, "firstname") %></b></td></tr>
  
  <tr><td>
      <span style="color:blue;"><i>
      <%# DataBinder.Eval(Container.DataItem, "Title")%></i></span>
      <p><img style="float:right;" src='image.aspx?
          id=<%# DataBinder.Eval(Container.DataItem, "employeeid")%>' />
      <%# DataBinder.Eval(Container.DataItem, "Notes") %></td></tr>
</table>
</ItemTemplate>
</asp:datalist>

It displays name and title of the employee in the first row of the table and then the picture surrounded by the notes. The picture is retrieved using a special .aspx page that returns JPEG data fetched from the database.

The pager can be placed anywhere in the page. In this case let's place it just above the buddy DataList control.

ms972960.pagercontrols_fig03(en-us,MSDN.10).gif

Figure 3. The SqlPager pages a DataList control

Does it make sense using the SqlPager control with a DataGrid control? It depends. The DataGrid already comes with an embedded paging engine based on the same PagedDataSource object I used here. So as long as you need to page through a single set of records displayed in a tabular format, the SqlPager is unnecessary. However, in master/detail scenarios, using the two controls together is not a farfetched idea. For example, if you add to the previous screenshot a DataGrid to display the orders managed by the employee, you have two related paging engines in the same page—one that pages through employees and one to scroll the related orders.

Summary

No matter what type of application you are building—a Web application, a Microsoft® Windows® application, or a Web service—you can rarely afford downloading and caching the whole data source you are expected to display. Sometimes, test environments might lead you to believe such a solution works great and is preferable. But test environments can be misleading. The size of the data source does matter, and the more the application must be scaled, the more size matters.

In ASP.NET, only the DataGrid control has built-in paging capabilities. However, a paging engine is made of rather boilerplate code and, with a bit of work, can be generalized and adapted to work with several different controls. The SqlPager control presented in this article does just that. It takes care of downloading the data and cuts it into pages to display through a buddy control. The control can retrieve and cache the whole data set or just ask SQL Server for the few records to display in the selected page. I said SQL Server, and that's another important point. The SqlPager works only with SQL Server and can't be used to retrieve data using OLE DB or ODBC. Nor can you use it to access Oracle or DB2 archives.

To make a really generic SQL pager component, you should generalize the data access layer and build a sort of factory class that creates connections, commands, and adapters using the appropriate data provider. On the other hand, bear in mind that setting up a paging engine for various SQL sources is worse than your worst nightmare. The approach presented here works only for SQL Server 7.0 and newer. The TOP clause is the discriminatory feature. Using server cursors and temporary tables, it can be adapted to a larger range of DBMS systems. But that would make your code significantly more complex.

About the Author

Dino Esposito is a trainer and consultant based in Rome, Italy. Member of the Wintellect team, Dino specializes in ASP.NET and ADO.NET, and spends most of his time teaching and consulting across Europe and the United States. In particular, Dino manages the ADO.NET courseware for Wintellect and writes the Cutting Edge column for MSDN Magazine.

© Microsoft Corporation. All rights reserved.