Freeze the Header, Scroll the Grid: Part 2

Make the DataGrid control fully scrollable.

CoreCoder

Languages: C#

ASP.NET VERSIONS: 1.0 | 1.1

 

Freeze the Header, Scroll the Grid: Part 2

Make the DataGrid control fully scrollable.

 

By Dino Esposito

 

In Freeze the Header, Scroll the Grid, I demonstrated how to build an automatic self-contained mechanism to make a grid of data scroll within a browser. Any HTML text that displays through a block element (for example, a

tag) can easily be made scrollable by simply using a style attribute. The rub lies in the fact that any contained HTML text can scroll. As I discussed last month, in some cases, you'll need to scroll the HTML text a Web page generates only partially. The DataGrid server control renders to the browser as an HTML table with a header, rows, and an optional footer and pager. The HTML text is output in a single shot when the server control enters its rendering phase. If you wrap the DataGrid component in a scrollable tag, all the HTML generated for the grid becomes scrollable, including the header and footer. In Part 1, I designed a user control that builds a small tree of controls so that the grid and its header are separate HTML blocks with distinct scrolling capabilities.

 

What was wrong with this? The final effect is just fine: The rows of the grid scroll; the header is stationary. To insert such a scrollable grid in a Web page, you have to import a user control (scrollablegrid.ascx). This control encapsulates the DataGrid control and makes it programmatically inaccessible from the outside world. You could extend the user control and make it expose the DataGrid as a public property; however, in this case, you can't declaratively program against the grid. In fact, there's no way you can add columns in the ASPX layout. But either way is okay for setting up a scrollable grid: You expose the internal DataGrid as an object and define columns programmatically, or bind the grid to an autogenerated set of columns.

 

In this article, I'll show you how to work around this issue. You'll extend the DataGrid control to support scrollable areas while preserving the same capabilities and programming facilities for the base class.

 

Enhance the DataGrid

To start out, let's create a new Web control and make it inherit from System.Web.UI.WebControls.DataGrid. Compared with the base class, the new control (which we'll call "MyDataGrid") looks different in a couple of ways. It exposes a new Boolean property (Scrollable) and overrides the method that governs the rendering process (Render). The Scrollable property defaults to false and determines the nature of the resultant HTML. If the grid doesn't have to scroll, the output of MyDataGrid matches the output of the base class perfectly. If the grid does scroll, the Render method creates an outermost table with three rows: the header, embedded grid, and footer. The embedded grid is the base class' output. It goes without saying that the embedded grid will be wrapped by a scrollable panel and rendered without a header or footer. The rest of the control's code will create and manage the header and footer as separate entities. This code snippet illustrates the HTML layout of the output the MyDataGrid control is going to generate:

 

   

   

   

Col1Col2

       

       

             grid

       

       

   

footer

 

A grid of data is rendered through an HTML table. To make an HTML table scrollable, you need to separate it into parts - a header, a footer, and rows, at a minimum. Although they're implemented using separate HTML tables, all these components are logically part of the same table and, to the extent that it is possible, should be rendered as a monolithic table. The biggest problem you face revolves around how the cells of the various tables align. For a good graphical result, you must ensure that the header cells are as large as the data cells. In addition, the rightmost header cell should be enlarged to encompass the pixels of the scrollbar (usually 16 pixels).

 

In my November column, I made an important assumption to smooth this difficulty. I assumed that the data source of the grid control had to be a DataTable. By simply running a couple of loops in the set accessor of the user control's DataSource property, I was able to determine the largest cell in the body and the pixels required by the corresponding header. The greater of the two determines the width of the column. This approach served a particular case: the case when AutoGenerateColumns is set to true.

 

Overall, the user control I described in the November issue is ideal when you don't need to bind specific columns with specific properties but do need to simply auto-generate the column based on the bound data source - a DataTable object. A DataGrid control (as well as any derived class) must be able to work with any bindable data source. You'll need to remember that the feasible data sources in .NET are those classes that expose (directly or indirectly) the IEnumerable interface.

 

As I've mentioned, the MyDataGrid control has three logical components: a header, a footer, and rows. The header and footer are created according to nearly identical logic. The rows are just the output of the base DataGrid embedded in an outer table. I've included the source code of the overridden Render method in Figure 1.

 

protected override void Render(HtmlTextWriter output)

{

  if (!Scrollable) {

    base.Render(output);

    return;

  }

 

  // Prepare the data source

  PagedDataSource pagedDataSource;

  pagedDataSource = CreatePagedDataSource();

  cols = CreateColumnSet(pagedDataSource,

           this.AutoGenerateColumns);

        

  // Create the outermost table

  Table outerTable = new Table();

  outerTable.CellPadding = this.CellPadding;

  outerTable.CellSpacing = this.CellSpacing;

  outerTable.Font.CopyFrom(this.Font);

  outerTable.BorderWidth = this.BorderWidth;

  outerTable.BorderColor = this.BorderColor;

  outerTable.BorderStyle = this.BorderStyle;

 

   // Add the various rows

  TableRow headerRow = new TableRow();

  TableRow gridRow = new TableRow();

  TableRow footerRow = new TableRow();

  if (ShowHeader)

    outerTable.Rows.Add(headerRow);

  outerTable.Rows.Add(gridRow);

  if (ShowFooter)

    outerTable.Rows.Add(footerRow);

 

  // Build the content of the table rows

  if (ShowHeader)

    BuildHeader(headerRow, cols);

  BuildGrid(gridRow, cols.Count);

  if (ShowFooter)

    BuildFooter(footerRow, cols);

 

  // Render to the writer

  outerTable.RenderControl(output);

}

Figure 1. The Render method is the heart of the rendering engine for ASP.NET server controls. By overriding Render, control developers can modify the way in which a control outputs its contents.

 

If the scrollable attribute has not been assigned to the grid, the Render method behaves as usual. If it has been assigned to the grid, it creates a new table and adds three rows to it. The table inherits some visual settings from the MyDataGrid control (for example, a border, a font, and colors). The first row represents the header and is added only if the ShowHeader property of the MyDataGrid control is true. Likewise, the third row represents the footer and is added according to the value of the ShowFooter property. The central row contains the rows of the grid (no header and footer here) and is the default output of the base DataGrid control - it's just what base.Render returns.

 

Capture a Control's HTML Output

The MyDataGrid control's output is obtained by combining the default output of a DataGrid with an HTML table element. The HTML output of any ASP.NET control can be obtained with a call to its RenderControl method. RenderControl is a public method defined on the base Control class. The method outputs the control content to a provided HtmlTextWriter object:

 

public void RenderControl( HtmlTextWriter writer );

 

There are two ways to surround this text with other HTML text: You can send text directly to the writer or you can build the control hierarchy first and then render it to the writer. The former approach results in slightly faster code because no extra server controls need to be instantiated to build the hierarchy. This code snippet shows how to output a table row:

 

// Wraps the output of a control in a table

output.WriteFullBeginTag("table");

output.WriteFullBeginTag("tr");

output.WriteEndFullBeginTag("td");

base.Render(output);

output.WriteEndTag("td");

output.WriteEndTag("tr");

output.WriteEndTag("table");

 

Building an in-memory hierarchy of server controls is more effective if you need to exercise strict control over the various elements. In this case, working with control properties is preferable to generating HTML text based on a forest of ifs and thens. The MyDataGrid control builds a Table control dynamically and adds rows to it. The central row is aimed at containing the items of the grid in a scrollable area. How can you insert the base grid's HTML text into this newly created and all-encompassing table? This is an instance of a more general problem: How can you capture an ASP.NET server control's HTML output?

 

The trick performed here exploits one of the constructors of the HtmlTextWriter class. The idea is that you call the Render method - the official way of generating HTML for a server control - on a canvas that's different from the HTTP response stream. The Render method accepts only an HtmlTextWriter object, but an instance of the class can be constructed to operate on an alternative stream; see this code snippet:

 

StringWriter str = new StringWriter(new StringBuilder());

HtmlTextWriter wr = new HtmlTextWriter(str);

base.Render(wr);

gridSource = str.ToString();

str.Close();

 

The HtmlTextWriter class' constructor takes a TextWriter object that represents the physical surface the output is sent to. In the default case, the text writer is an instance of the HttpWriter class. Nothing prevents you from using a different text writer, however. In particular, you can use a StringWriter object. The StringWriter class inherits from TextWriter and stores all the received text to an internal string builder. To obtain the string that represents the output of the control, you simply issue a call to the StringWriter's ToString method. The text obtained in this way is then stuffed into the unique cell of the central row. The cell spans over the number of columns that form the bound data source. Because the header and the footer of the grid are created externally, you should make sure that ShowHeader and ShowFooter are set to false when you render the base DataGrid to HTML.

 

Build the Header

The header and footer are rendered as parts of a table that are distinct from the DataGrid's table. You need to access the data source to know how many cells will be created and the width of each. For the sake of simplicity, I'll consider the case where AutoGenerateColumns is false and columns are explicitly bound with a width in pixels. If you like a more general solution, you should calculate the maximum width of each bound text, including the header text, and programmatically assign that width to the various columns. (This is largely demonstrated in Part 1.)

 

So let's see how to build the header and footer assuming that we know how many columns are bound and their requested width. The first step is to apply the grid's header style to the table row that forms the header:

 

row.ApplyStyle(this.HeaderStyle);

 

You'll need to generate a cell for each column that's bound. If columns have been statically bound using the tag, then the Columns collection will track them all. Otherwise, you should call an internal protected member to create the column set. This is necessary if you want to consider a more general solution that fully supports the auto-generation of columns. The method to call is CreateColumnSet and requires a PagedDataSource object:

 

ArrayList cols = new ArrayList();

PagedDataSource pagedSource = CreatePagedDataSource();

cols = CreateColumnSet(pagedSource, AutoGenerateColumns);

 

CreatePagedDataSource is an internal method (see Figure 2). It creates a paged data source on top of the bound data source.

 

private PagedDataSource CreatePagedDataSource()

{

  PagedDataSource source = new PagedDataSource();

  source.DataSource =

(IEnumerable) ResolveDataSource(this.DataSource);

  source.CurrentPageIndex = this.CurrentPageIndex;

  source.PageSize = this.PageSize;

  source.AllowPaging = this.AllowPaging;

  source.AllowCustomPaging = this.AllowCustomPaging;

  source.VirtualCount = this.VirtualItemCount;

  return source;

}

Figure 2. Transform the bound data source into a paged data source. A paged data source is a list of data items that provides support for paging.

 

As you can see in Figure 2, the PagedDataSource class is a container for the DataGrid properties that control paging. The DataGrid is the only server control that supports paging, but the PagedDataSource control can be used successfully with other list controls (for example, the DataList).

 

Comply With Design-Time Requirements

You'll see the final fully scrollable grid with a distinct header and footer (see Figure 3). Note that both rows can be hidden at will using the classic properties ShowHeader and ShowFooter.

 


Figure 3. The MyDataGrid control displays its data source using a scrollbar instead of a paging mechanism.

 

While I was developing the control, I didn't pay much attention to the look-and-feel of the control in the Visual Studio .NET project. When the control was ready to use, I created a new Web project to test it more effectively. To my great surprise, the design-time HTML text for the control was an error message. When an ASP.NET control is placed on a Web form, its RenderControl method is called, which in turn calls into Render. However, when this happens, the data source is not bound to the control unless you're using a constant DataSet created within the project. You must be aware of this and adjust the rendering code at design time. In particular, to force the control to display a scrollbar at design time, I programmatically gave the

tag a height smaller than the height of the control. The results are shown in Figure 4. Note that the best practice for design-time code is keeping it in a distinct class, and possibly a distinct assembly, to avoid adding unnecessary overhead.

 


Figure 4. The MyDataGrid control displays just fine in the Visual Studio .NET IDE.

 

Now that I've shown you how to extend the DataGrid control to make it fully scrollable, you're ready to refine it further to meet your own requirements, or to use it if it suits your needs. I've been practicing with scrollable grids for a while and formed the idea that a one-size-fits-all solution is hard to build. The reason is that HTML tables - the ultimate output of DataGrids - don't support scrolling for distinct components such as TBODY. To work around this, you must split the table in distinct sub-tables and strive to calculate effective column widths. You should use this code as a starting point for your own experimentation.

 

The sample code in this article is available for download.

 

Dino Esposito is a trainer and consultant who specializes in ASP.NET, ADO.NET, and XML. The author of Building Web Solutions with ASP.NET and ADO.NET and Programming Microsoft ASP.NET (both from Microsoft Press), Dino is also the cofounder of http://www.VB2TheMax.com. Write to him at mailto:[email protected].

 

 

 

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