Click for More Info

Enhance the DataGrid to Support Drop-down Views

CoreCoder

LANGUAGES: VB | JavaScript

ASP.NET VERSIONS: 1.0 | 1.1| 2.0

 

Click for More Info

Enhance the DataGrid to Support Drop-down Views

 

By Dino Esposito

 

Almost every day I receive messages from readers asking about cool new features to implement on a DataGrid control. To tackle the one discussed in this article, though, I didn't need any encouragement from eager readers. In fact, this feature has been at the top of my wish list for quite some time. More than once I have tried to figure out how to enhance a DataGrid control to support drop-down views, but I've always given up at the first sign of trouble.

 

What is this mysterious new DataGrid feature? Imagine you have a list of names; for example, customer names. You scroll your list and see only key information about each of them, such as name, ID, and perhaps country. But what if you need to access other, less-frequently-read information?

 

A typical solution entails adding a button to each row of your grid to let users click if they want a more detailed view. Although this solution is acceptable and, more importantly, works with all browsers, you still might want to exploit the browser's capabilities to come up with something more eye-catching.

 

In this article, I'll build a DataGrid that supports drop-down views of child rows. The idea is that you can click on a row and have an extra block of data with more detailed information about the current record appear just below the row. In the end, the DataGrid row is displayed using two rows, each with a different and customizable template.

 

Look Before You Leap

My first thought was to implement this through a templated column. Each cell of the templated column would be filled with a couple of rows: one showing the highlights of the records (as in a DataGrid default output) and one presenting the detailed view. I know; you're wondering how to add a "couple of rows" to a table cell? You can, for instance, embed a new table in the cell, or you can nest a couple of

tags. In both cases you must use explicit width values to make sure you get a final output close to that of the classic DataGrid.

 

Again, this may be acceptable, but not really desirable. To make a long story short, a templated column is an interesting approach if you want to build a custom list control using a DataGrid internally. However, if you only want to endow a DataGrid control with the capability of displaying drop-down views, you should look elsewhere. In my scenario, I only wanted a richer DataGrid control offering drill-down features without losing or limiting any of its core features.

 

This brings up the question of whether there is a way to render a record on two (or more) rows within a DataGrid? I have stated in previous books and articles that you can't (or at least shouldn't) add additional rows to a DataGrid at run time. In the end, however, creating new DataGrid rows dynamically looked like the only safe way to implement drop-down views in light of the requirements.

 

The scenario gets even more complicated because I want the second row of each displayed record to appear and disappear as the user clicks on the first row. To accomplish this, some client-side script code is required, thus reducing the number of browsers that support the feature.

 

Let's start by creating a new page with a DataGrid control inside (see Figure 1). The DataGrid has two columns and supports paging. As such, it is just an ordinary grid. With the addition of a few event handlers, though, you can transform it into a compelling drop-down grid like the one shown in Figure 2.

 

  onitemdatabound="ItemDataBound"

  onitemcreated="ItemCreated" ondatabinding="DataBinding"

  onpageindexchanged="PageIndexChanged"

  allowpaging="true" autogeneratecolumns="false">

  

    

      datafield="companyname" />

    

      datafield="country" />

  

Figure 1: Create a new page with a DataGrid control.

 


Figure 2: Hook up the data binding process of the DataGrid control and transform an ordinary grid into a cool drop-down grid control.

 

A drop-down DataGrid control renders each item data row with two table rows. The top row is the regular DataGrid row with as many cells as there are bound columns. The bottom row represents the detailed view of each displayed record. The layout of this row is customizable and is defined through an external user control (an ASCX file). The second row is initially hidden and is turned on and off using JavaScript code. The top row is generated as part of the default DataGrid's rendering process; the bottom row, in contrast, is dynamically created and added during the ItemDataBound event.

 

The ItemCreated event fires twice for a DataGrid control. It fires the first time when the page is reinitializing after a postback event. When this occurs, the DataGrid rebuilds its own user interface from the viewstate, and no access to the bound data source object is ever performed. This phase is particularly important for a drop-down DataGrid because at this time you must recreate any dynamically added extra row. There's no built-in way to distinguish the two invocations of the ItemCreated event. A possible workaround consists of using a global Boolean variable that is initialized to false and set to true only when the DataBinding event fires. This trick works well, because the second ItemCreated event isn't fired until the data binding process for the current request begins.

 

Add a Second Row

The DataGrid control renders out as a plain HTML table and determines its number of rows based on the declared size of the page (the PageSize property). By adding a second row per each data item, you double the size of the page. Tracking this dynamic change is extremely important to ensure that the DataGrid is correctly restored from the viewstate when the page posts back. Dynamic controls, in fact, are not automatically handled by the ASP.NET infrastructure, and need some help from the developer to restore nicely and effectively. Here's the outline of the ItemDataBound event handler:

 

Sub ItemDataBound( ... )

  If Not (e.Item.ItemType = ListItemType.Item) And Not _

      (e.Item.ItemType = ListItemType.AlternatingItem) Then

    Return

  End If

  CreateChildView(e.Item, ChildViewUrl, "none")

  grid.PageSize = 20  ' Double the declared size.

End Sub

 

The event handler creates the child, drop-down view and doubles the page size at the end of method. In this example, I use a constant page size of 10 and set the PageSize property to 20. Note that you can't simply double the current value of the PageSize property, as shown here:

 

grid.PageSize = 2 * grid.PageSize

 

ItemDataBound is iteratively invoked, so doubling the current value would exponentially increase the original size of the page.

 

The code in Figure 3 demonstrates how to add a dynamic row just below each data grid item. The new row is initially hidden and filled with the contents of an external ASCX user control. The user control is expected to show a more detailed view of the record being rendered. The user control receives the data to display directly from the DataGrid through a public method. You can require that any user control used for this purpose implement a given interface, or at least implement a public method with a certain name and signature:

 

Public Sub Initialize(items As Hashtable)

 

Here, the hashtable contains the name of a record field and the corresponding value. The user control will take these values and bind them to the matching user interface elements. Figure 3 shows the source code of a sample view control.

 

Sub CreateChildView(tr As DataGridItem, url As String)

  ' Create the new row.

  Dim newTR As New DataGridItem(tr.ItemIndex+1, -1,

                                ListItemType.Item)

  newTR.BackColor = Color.Snow

  newTR.ID = "View_" + tr.ItemIndex.ToString()

  ' Populate the new row.

  Dim td As New TableCell

  newTR.Cells.Add(td)

  td.ColumnSpan = grid.Columns.Count

  ' Add the row to the grid.

  Dim t As Table = CType(tr.Parent, Table)

  t.Rows.Add(newTR)

  ' Add the child view.

  AddUserControl(url, newTR, tr.DataItem)

  Dim js As String = "Toggle({0})"

  js = String.Format(js, newTR.ClientID)

  tr.Attributes("onclick") = js

End Sub

 

' Add the child view (an ASCX control).

Sub AddUserControl(url As String, tr As DataGridItem,

                   data As Object)

 

  ' Load and add the ASCX view control.

  Dim customView As UserControl = Page.LoadControl(url)

  tr.Style("display") = "none"

  tr.Cells(0).Controls.Add(customView)

  ' The ASCX control must expose the method Initialize().

  ' The method would take a collection of data to fill

  ' its user interface.

  Dim displayData As New Hashtable

  displayData.Add("customerid", _

                  DataBinder.Eval(data, "customerid"))

  displayData.Add("contactname", _

                   DataBinder.Eval(data, "contactname"))

  displayData.Add("companyname", _

                  DataBinder.Eval(data, "companyname"))

  displayData.Add("city", DataBinder.Eval(data, "city"))

  displayData.Add("address", _

                  DataBinder.Eval(data, "address"))

  displayData.Add("country", _

                  DataBinder.Eval(data, "country"))

  ' Initialize the custom control.

  ' ChildViewControl is the ASCX classname attribute.

  Dim ctl As ChildViewControl

  ctl = CType(customView, ChildViewControl)

  ctl.Initialize(displayData)

End Sub

Figure 3: Create a drop-down view by adding a second table row to the current grid item.

 

The child view control is loaded from an external .ascx file using the Page.LoadControl method. The method returns a UserControl object. How can you cast it to the specific user control class and programmatically access the Initialize method? The user control class name is set in the @Control directive, as Figure 4 demonstrates. But how would you reference the container assembly in the current page? Pretty simple: Use the @Reference directive in the host page:

 

<%@Reference Control="view.ascx" %>

 

The @Reference directive automatically brings in the assembly where the specified control is compiled. In this way, you can programmatically use instances of the control class, and call specific methods and properties.

 

<%@ Control Language="VB" ClassName="ChildViewControl" %>

 

 

  

  

    

    

  

      

      

  

  

    

  

Figure 4: The source code of the sample view control.

 

Restore from Viewstate

The code in Figure 3 is invoked within the ItemDataBound event, which, in turn, is invoked during the data binding process. When the data binding starts, you must reset the PageSize value to avoid serious problems with the layout of the grid.

 

What happens, instead, when the page (and the contained DataGrid control) is built from the viewstate? Data-bound controls should always pay attention to postback events caused by other controls in the same page. When a data-bound control posts back, it's natural to rebind it to the data source and trigger the data binding process. When the data-bound control undergoes, but doesn't directly cause, a postback event, there's no data source object to provide its data for display. In this case, the control must be able to restore itself using any information that its constituent elements stored in the viewstate. The first call to the ItemCreated event handles this situation.

 

When you create the grid for the first time, you have a doubled PageSize value and a double number of data items. This information is correctly restored from the viewstate and a table with 20 rows is created, 10 of which are hidden. However, none of the original settings of the second row have been maintained. This has two practical effects. First, the second column (dynamically added during the previous ItemDataBound) is empty. Second, the row contains the same number of cells as the preceding row. By design, instead, the second row should contain only one cell.

 

In the first call to ItemCreated remove all unnecessary cells, set ColumnSpan properly, and reload the user control (see Figure 5).

 

Sub ItemCreated(Sender As Object,

                e As DataGridItemEventArgs)

  If m_binding Then

    Return

  End If

  ' Process the second row of each pair.

  If e.Item.ItemIndex Mod 2 > 0 Then

    e.Item.Cells(0).ColumnSpan = grid.Columns.Count

    For i As Integer = 1 To e.Item.Cells.Count-1

      e.Item.Cells.RemoveAt(1)

    Next

    ' Reload the ASCX.

    AddUserControl(ChildViewUrl, e.Item, Nothing)

    e.Item.ID = "View_" + (e.Item.ItemIndex\2).ToString()

  End If

End Sub

Figure 5: This routine  calls ItemCreated, removes all unnecessary cells, sets ColumnSpan properly, and reloads the user control.

 

In addition to reloading the user control, the ItemCreated event also refreshes its ID. This is important because it toggles the visibility of the child view control.

 

The Final Touch

Immediately after being instantiated, the custom view control is given a unique ID. The ID is needed to uniquely identify the row on the client, and easily implement an expand/collapse feature. Here's the JavaScript function that does it:

 

function Toggle(o)

{

  if (o.style["display"] == null)

    o.style["display"] = '';

  if (o.style["display"] == '' )

    o.style["display"] = 'none';

  else

    o.style["display"] = '';

}

 

The Toggle function receives the ID of the child view to show or hide. The CSS display style is used to toggle the visibility of the row. The Toggle function is associated with the onClick event on the native DataGrid row - the parent row of the drop-down view:

 

Dim js As String = "Toggle({0})"

js = String.Format(js, trChildView.ClientID)

trParent.Attributes("onClick") = js

 

Because the onClick handler is defined for the whole table row, you can click everywhere in the row and still execute the JavaScript code.

 

Conclusion

A DataGrid with drop-down views is helpful when you have a rich browser, and a table with many columns to display. You can use the standard interface of the DataGrid to display the most important fields, and resort to a drop-down customizable child view for all the others.

 

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. Author of Programming Microsoft ASP.NET (Microsoft Press), Dino is also the cofounder of http://www.VB2TheMax.com. Write to him at mailto:[email protected] or join the blog at http://weblogs.asp.net/despos.

 

 

 

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