Logical Navigation

Walk through a DataGrid Using Variable-length Pages

DataBound

LANGUAGES: C#

TECHNOLOGIES: DataGrid Control

 

Logical Navigation

Walk through a DataGrid Using Variable-length Pages

 

By Dino Esposito

 

Of all the ASP.NET list-bound controls, only the DataGrid control supports data paging. The Repeater control is too basic and schematic to provide for such an advanced feature, and the DataList control is intended more for rendering a list of rows according to a custom layout. The DataList's lack of support for paging is more a design choice than a real limitation of the control's interface. But, all in all, the DataList control is only one step away from supporting button-driven navigation, and you could add it with surprisingly short and simple code.

 

The DataGrid control, the only .NET data-bound Web control capable of paging through records, provides two basic types of paging. They differ in the way in which the page content is retrieved. Default paging requires you to bind the control to the whole dataset and then leave the control in charge to select the subset of records that will fit in each displayed page. Custom paging is based on the assumption that the programmer puts into the control's data source exactly those records that should be displayed in the current page. Will this cover all your needs when it comes to planning navigation through a dataset? Well, the types of paging you get for free from the DataGrid control certainly do not cover all the real-world scenarios you might face. On the other hand, by exploiting the DataGrid's custom-paging infrastructure, you easily can build new paging engines that fit into your application's architecture.

 

In this article, I'll demonstrate how to use the DataGrid's custom-paging mechanism to set up a sort of chapter paging - that is, a navigation engine that displays variable-length pages in which the rows are selected based on a logical condition rather than a physical position.

 

Chapter-paging Basics

In the DataGrid control, all pages but the last have to be of the same size. You specify the page size through the PageSize property. When you move from one page to another, you indicate the next page by number. Suppose you click on the link that represents page 3. As a result, the DataGrid determines the rows to display. The control discards the first PageSize*(3-1) records and selects the next PageSize records. If custom paging is turned on, the DataGrid control also needs to know the size of the data source (the VirtualItemCount property) and assumes the developer put in the data source the subset of rows to display for the current page.

 

In general, the paging mechanism allows users to scroll the results of a single query. In cases in which a query may select a large number of rows, you predispose a software infrastructure so users see the results a bit at a time.

 

How is chapter paging different? Suppose you have a query that selects the orders of a particular customer for a given year. If you run a similar query on the SQL Server 2000 Northwind database - that is, a realistic but sample database - you get a few hundred rows. So, it seems to incarnate perfectly situations in which you need to use paging. But are you really sure your users would love to scroll a year's orders by page? How could they organize the results if you showed them a long list of page indexes, say 30 or 40? Wouldn't it be much better if you grouped orders by month? In traditional paging, you keep the page size constant but provide a variable-length pager bar. Chapter paging does the opposite. It keeps the pager bar's size constant but accepts pages of any length.

 

I'll describe the logical navigation through chapters for orders and months, but the mechanism is rather general and applies to a number of everyday scenarios: grouping customers or contacts by initials, sales by year or quarters, or bills by weeks or days.

 

Implementing page-based data navigation means that you filter your original result set using an extra condition that's more restrictive. With chapter paging, such an additional condition is logical and data-specific rather than physical and data agnostic.

 

Setting up the Solution

In the sample code, I accessed the Northwind orders for a given year and then let users select resultant orders on a per-month basis. The SQL query I used is shown here:

 

SELECT o.orderid, o.orderdate, c.companyname

FROM orders AS o

INNER JOIN customers AS c ON o.customerid=c.customerid

WHERE Year(orderdate)[email protected]

 

I also configured the target DataGrid to support custom paging. Note the settings for the pager bar in FIGURE 1:

 

  AllowPaging="True"

  AllowCustomPaging="True"

  PageSize="100"

  OnItemCreated="ItemCreated"

  OnPageIndexChanged="PageIndexChanged">

 

  

          PageButtonCount="12" />

  

  

  :

  

FIGURE 1: Setting up the DataGrid and pager bar.

 

Because the number of items in the displayed pages varies (and is potentially lengthy), you might want to place the pager bar at the top of the grid to simplify the user's task so he or she doesn't have to scroll to locate the links to a page. The pager would work reasonably in numeric-page mode even though nothing really prevents you from coding a paging mechanism using next- or previous-month links.

 

Although you could manage to navigate through variable-length pages, the DataGrid's custom-paging run time is designed to show equally sized pages (with the obvious exception of the final one). This means each grid page always will show PageSize items, no matter how many items are stored in the data source. So, you must configure the DataGrid to host the maximum number of items your logical page ever will contain, and adjust the number of links in the pager bar accordingly. Unless you know the exact number of items each page will contain - this is application-specific - you have to resort to an estimate. I have chosen to show no more than 100 records per page. Subsequently, to ensure that exactly 12 links will appear on the pager bar, I set VirtualItemCount to 1,200 - specifically 100 x 12. Furthermore, consider that by default, the DataGrid does not display more than 10 links on the pager. To increase this number, just set the PageButtonCount property to 12. In its simplest form, before undergoing a series of graphical and functional enhancements, the DataGrid looks like FIGURE 2.

 


FIGURE 2: Each link in the pager bar refers to a month.

 

The grid's pager bar contains numbers from one to 12. Each index refers to one of the pages in the data source. However, thanks to the settings I described already, each index also identifies a month. In general, the page index works as the selector to access some key information you use to populate the page. In this case, there is a natural one-to-one correspondence between page and month indexes.

 

For instance, if you were paging a list of contacts by their initials, you could have found more useful a conversion array, such as in FIGURE 3:

 

if (!IsPostBack)

{

   ArrayList a = new ArrayList();

   a.Add("a-b-c-d");   // page 1

   a.Add("e-f-g-h");   // page 2

   a.Add("i-j-k-l");   // page 3

   a.Add("m-n-o");     // page 4

   a.Add("p-q-r");     // page 5

   a.Add("s-t-u");     // page 6

   a.Add("v-w-x-y-z");   // page 7

   ViewState["PageMap"] = a;

}

FIGURE 3: An array that maps page indexes to key information useful to query for page-specific data.

 

The 0-based page index is used to access the dash-separated string containing the initials. Then, the characters are used to set up a proper SQL command to fill the page. You retrieve the initials for the current page with the following code:

 

strInitials = ((ArrayList)ViewState["PageMap"]) _

  [grid.CurrentPageIndex].ToString();

 

Notice that you have to store the conversion array into the ASP.NET page's ViewState in order to make it persistent across multiple page requests. The following SQL statement shows a possible way to turn initials into displayable rows:

 

SELECT * FROM customers

WHERE customerid LIKE 'A%' OR customerid LIKE 'B%'

      OR customerid LIKE 'C%'

 

I'll show this in action later. For now, I'll switch back to the orders example. You use the page index as the month index and grab the rows to display by extending the query shown earlier:

 

SELECT o.orderid, o.orderdate, c.companyname

FROM orders AS o

INNER JOIN customers AS c ON o.customerid=c.customerid

WHERE Year(orderdate)[email protected]

AND Month(orderdate)[email protected]

 

Basically, each time the user clicks to move to a new page, the above command runs using the new page index as one of the parameters. The other parameter is the year of reference - information obtained from a drop-down list in the page.

 

Pager-bar Manipulation

The data grid shown in FIGURE 1 is functional but not particularly attractive. To make it more compelling, you need to hook up the ItemCreated event and get into the game when the type of the item being created is ListItemType.Pager:

 

public void ItemCreated(Object sender,

 DataGridItemEventArgs e)

{

  ListItemType lit = e.Item.ItemType;

  if (lit == ListItemType.Pager)

  {...}

}

 

The idea is to replace the numeric text that represents each page with a text that represents the corresponding month. In doing so, you also may want to sharpen the distinction between clickable months (rendered as LinkButton objects) and the current month, which is rendered through a Label control. In HTML code, the pager bar evaluates to a tag, a TableCell in ASP.NET:

 

TableCell pager = (TableCell) e.Item.Controls[0];

for(int i=0; i

{

  Object o = pager.Controls[i];

  :

}

 

The various constituent controls of the pager - link buttons and labels - are separated by blank literal controls, and this is why the loop above steps every two controls. To convert the page index into the related month name, you create a temporary DateTime object given by valid day and year information and the specified month. Then, the date object is formatted to an ad hoc string in such a way that only the three-letter abbreviation for the month is rendered. This text is assigned to the link button:

 

if (o is LinkButton)

{

  LinkButton lb = (LinkButton) o;

  DateTime dt = new DateTime(2002,

   Convert.ToInt32(lb.Text), 1);

  lb.Text = dt.ToString("MMM");

}

 

When the pager control is a Label, you get the corresponding month name using the same technique but apply some extra visual settings: a background color and bold font. FIGURE 4 shows the final results.

 


FIGURE 4: A new pager bar with month names.

 

Visual Adjustments

In FIGURE 4, you can observe a nice complementary effect. The selected month looks like the selected tab of a tab strip. To get this, start by setting the DataGrid's CellSpacing and CellPadding properties to 0. However, in doing so, you end up having no space left between the various rows of the grid. Although this feature does not affect the functionality of the solution, very tight rows certainly do not improve the overall readability of the grid contents. To space out the rows without changing either cell padding or cell spacing, you can alter the item's Height property:

 

 

If you would like to improve the look and feel of the pager with fake tabs, you can use the border-related properties of the Web controls. For example, links could be customized as follows:

 

lb.BorderWidth = 1;

lb.BorderColor = Color.White;

lb.BorderStyle = BorderStyle.Outset;

lb.BackColor = Color.LightYellow;

 

A light 3-D border now frames the text and makes it look like a tab. Similarly, you adjust the visual settings of the label:

 

l.BorderWidth = 1;

l.Style["border-top"] = "SkyBlue outset 1px";

l.Style["border-left"] = "SkyBlue outset 1px";

l.Style["border-right"] = "SkyBlue outset 1px";

l.Style["border-bottom-color"] = "SkyBlue";

 

In this case, to simulate continuity (and contiguity) between the selected tab and the rest of the control, you render the bottom border with a different color. To do so, though, you must resort to direct CSS style settings. The Web control's Style property serves just this purpose. FIGURE 5 shows the final results on an ASP.NET page that demonstrates the applicability of the chapter paging to different scenarios.

 


FIGURE 5: A tabbed list of customers.

 

Conclusion

In the accompanying source code, you'll find two aspx pages, one of which illustrates the orders sample as I described in the article. The second ASP.NET page demonstrates how to apply chapter paging to a list of customer names using a group of initials to create logical pages. Although it's functional in many cases, this approach is not free from problems. In particular, how would you deal with source pages whose lengths reasonably require further navigation? If you come up with 1,000 orders in a month, you must choose a different approach. I know I leave you with quite a teaser, but don't worry. I'll tackle that topic in a future article. In my next column, I'll wrap all the code I used here in a custom control.

 

The files referenced in this article are available for download.

 

Dino Esposito is a trainer and consultant for Wintellect (http:// www.wintellect.com) where he manages the ADO.NET class. Dino writes the "Cutting Edge" column for MSDN Magazine and "Diving into Data Access" for MSDN Voices. Author of Building Web Solutions with ASP.NET and ADO.NET from Microsoft Press, Dino is also the cofounder of http://www.VB2TheMax.com. Write to him at mailto:[email protected].

 

Tell us what you think! Please send any comments about this article to [email protected]. Please include the article title and author.

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish