Creating Custom Columns for the ASP.NET Datagrid
Summary: The ASP.NET Datagrid control is one of the more powerful controls available to the Web developer. See how you can make it even more powerful by adding custom columns to display your data more appropriately for your users. (16 printed pages)
The Case for Reuse
The DropDownList Scenario
The Built-In Datagrid Columns
The Old-Fashioned Way: DropDownList in a TemplateColumn
The Mother of All Columns: DataGridColumn
The Reusable Way: Creating the DropDownColumn
Using the DropDownColumn
Let's face it, writing Datagrid code for Microsoft® ASP.NET can be pretty repetitive. Even as obsessed with this control as I am, I'm always looking for shortcuts to make these tasks easier. And no one enjoys writing repetitive code, right? To help avoid this despicable task, the goal is to take code that repeats across projects and encapsulate it into one nice, cohesive package that you can reuse over and over again. That's what object-oriented programming is all about, and working with the Datagrid is no exception. One way to accomplish this with the Datagrid control is to take code you would commonly use with one of the built-in column types (which I'll do in the first example), and create a custom column type that can then be reused in multiple Datagrid projects.
This article will walk you through the process of working with a DropDownList control inside one of your Datagrid columns using a standard TemplateColumn, and then converting that logic into your very own custom column type for the Datagrid, which I'll call the DropDownColumn. There are also some freely available custom Datagrid columns already created for you, so be sure to check out what's there for download at MetaBuilders.com.
If your group is like many organizations, you've already separated your business logic and/or data access logic into a separate component or set of components, and the remaining ASP.NET files, ASPX, and code-behind contain purely presentation logic. ("Purely" being a relative term.) However, even that presentation-level logic should be reused when possible, so that the next time a user comes and knocks on your door saying, "Hey, I want my application to look just like Susie's in Accounting," you can build her something quickly, reusing some of the presentation elements from the Accounting application. Or maybe you want to package up some logic to sell on the Web or distribute on your Web site. ASP.NET makes this easier than ever with the ability to create your own server controls, or to derive from existing types to get the functionality that you need.
Say you're editing—what else—the Northwind database on your local Microsoft SQL Server™, and you want to give your user (let's call him Ed the Fulfillment Manager) the ability to edit the Orders table. One of the fields contains the shipping information (ShipVia), which Ed needs to be able to modify. In display mode, the shipping company should be displayed as plain text. When Ed clicks the Edit button, rather than provide him with just a TextBox to edit the shipping method code (1, 2, or 3), you want to give him a DropDownList containing the different shipping companies he can choose from. (Ed can't remember which shipping company corresponds to which number anyway, so this helps him out.)
Figure 1. Selecting a shipping company
Now that we've reviewed the problem situation, let's take a step back and look at the 5 column types for Datagrid columns built into ASP.NET, and their parent class, DataGridColumn.
- BoundColumn. This is for standard display of textual fields. It renders as plain text but converts to a TextBox when the Datagrid is placed into Edit mode. Some formatting options are also available.
- HyperlinkColumn. This is for display of text data, which also represents a Web address (URL). The URL may or may not be the same as the display text; both can be set independently. It renders as an <a href=...> tag.
- ButtonColumn. This is for allowing user interaction with the grid on a row basis. It can be rendered as either a hyperlinked LinkButton (<a href=...>), or a Pushbutton (<input type="button">). A PostBack is triggered when the button is clicked, and the ItemCommand event is fired on the Datagrid.
- EditCommandColumn. This is similar to ButtonColumn, but it automatically creates buttons for editing the Datagrid, and for canceling or submitting the changes made. Fires the ItemCommand event, plus the specific event for the button clicked, EditCommand, CancelCommand, or UpdateCommand.
- TemplateColumn. This is for full control over what controls are displayed to the user, divided into different templates, such as ItemTemplate and EditItemTemplate. Any ASP.NET or HTML control or set of controls can be placed inside those templates.
Note Before working with these column types directly, be sure to turn off "Generate columns automatically at run time" (AutoGenerateColumns). You can then work with these column types either in the Property Builder, or directly in the HTML code of your ASPX file.
Figure 2. Five built-in columns, inherited from DataGridColumn
As useful as these column types are, they're just the beginning of what's possible inside of a Datagrid column.
Before delving into creating the new column type, first let's look at how to solve the dropdown problem without a custom column, by working with a DropDownList directly inside a TemplateColumn. The ItemTemplate will contain only the plain-text representation of the current value, while the EditItemTemplate will include an <asp:DropDownList> control that you'll manipulate at runtime.
<asp:DataGrid id="DataGrid1" runat="server" CssClass="grid" AutoGenerateColumns="False"> <Columns> <asp:EditCommandColumn EditText="Edit" CancelText="Cancel" UpdateText="Update" /> <asp:BoundColumn DataField="OrderID" ReadOnly="True" HeaderText="Order ID" /> <asp:BoundColumn DataField="ShipName" HeaderText="Ship to" ReadOnly="True" /> <asp:BoundColumn DataField="ShipCountry" HeaderText="Country" ReadOnly="True" /> <asp:TemplateColumn HeaderText="Ship Method"> <ItemTemplate> <%#Container.DataItem("ShipVia")%> </ItemTemplate> <EditItemTemplate> <asp:DropDownList runat="server" ID="Dropdownlist1"/> </EditItemTemplate> </asp:TemplateColumn> </Columns> </asp:DataGrid>
Code to bind the Datagrid:
Sub BindGrid() Dim SQL As String = "SELECT OrderID, ShipName, ShipCountry, ShipVia FROM Orders" Dim DA As SqlDataAdapter = New SqlDataAdapter(SQL, ConnStr) Dim DS As New DataSet DA.Fill(DS, "Orders") DataGrid1.DataSource = DS.Tables("Orders").DefaultView DataGrid1.DataBind() End Sub
The binding of the DropDownList for the item currently being edited happens during the Datagrid's ItemDataBound event. When using the ItemDataBound event, always be sure to check the ListItemType of the current item, or you can quickly find yourself working with a HeaderItem, or any other non-applicable item type. Get a reference to the DropDownList control for the EditItem. In the code below, I've shown this using the cell's controls collection directly (for consistency with later examples), but you could do this just as easily by assigning an ID to the DropDownList control, and locating the control reference using the FindControl method of the Datagrid item. Since the Datagrid is bound to the default view of a DataTable, each element of that view is of type DataRowView, and you can cast the current item's DataItem property to an instance of DataRowView. This lets you refer to fields in that DataItem directly by their field name. Using this technique, store the current value for "ShipVia" on this record, and use it to select the appropriate dropdown item.
Private Sub DataGrid1_ItemDataBound(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.DataGridItemEventArgs) _ Handles DataGrid1.ItemDataBound If e.Item.ItemType = ListItemType.EditItem Then Dim DRV As DataRowView = CType(e.Item.DataItem, DataRowView) Dim CurrentShip As String = DRV("ShipVia") Dim DDL As DropDownList = _ CType(e.Item.Cells(4).Controls(1), DropDownList) Dim SQL As String = _ "SELECT ShipperID, CompanyName FROM Shippers ORDER BY ShipperID" Dim DA As SqlDataAdapter = New SqlDataAdapter(SQL, ConnStr) Dim DS As New DataSet Dim item As ListItem DA.Fill(DS, "Shippers") DDL.DataSource = DS.Tables("Shippers").DefaultView DDL.DataTextField = "CompanyName" DDL.DataValueField = "ShipperID" DDL.DataBind() item = DDL.Items.FindByValue(CurrentShip) If Not item Is Nothing Then item.Selected = True End If End Sub
And finally, the code to retrieve the currently selected value out of the DropDownList, and execute the update back to the database:
Private Sub DataGrid1_UpdateCommand(ByVal source As Object, _ ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) _ Handles DataGrid1.UpdateCommand Dim DDL As DropDownList = _ CType(e.Item.Cells(4).Controls(1), DropDownList) Dim NewShip As Integer = DDL.SelectedValue Dim OrderID As Integer = Int32.Parse(e.Item.Cells(1).Text) Dim SQL As String = _ "UPDATE Orders SET ShipVia=@Ship WHERE OrderID=@ID" Dim Conn As SqlConnection = New SqlConnection(ConnStr) Dim Cmd As New SqlCommand(SQL, Conn) Cmd.Parameters.Add(New SqlParameter("@Ship", NewShip)) Cmd.Parameters.Add(New SqlParameter("@ID", OrderID)) Conn.Open() Cmd.ExecuteNonQuery() Conn.Close() DataGrid1.EditItemIndex = -1 BindGrid() End Sub
Let's go back and examine the parent class of all the built-in column types, DataGridColumn. (See Figure 1.) She contains the properties and methods common across all Datagrid columns. I've placed a star next to the ones you'll make use of in your custom column type that you'll create.
- FooterStyle (TableItemStyle)
- FooterText (String)
- HeaderImageUrl (String)
- HeaderStyle (TableItemStyle)
- HeaderText (String)
- ItemStyle (TableItemStyle)
- SortExpression (String)
- Visible (Boolean)
The first thing you'll need to do is create a new Class Library project in Microsoft® Visual Studio® .NET and name it MyCustomColumn. Add a new class called DropDownColumn, and make sure to add the namespace around your class definition, so your initial code will look like this:
Namespace MyCustomColumn Public Class DropDownColumn Inherits DataGridColumn Public DataSource As ICollection Public DataField As String Public DataTextField As String Public DataValueField As String End Class End Namespace
I've also declared 4 public properties there to be used as follows:
- DataSource. This is the set of data to populate the DropDownList. This can be anything that implements the ICollection interface. For the examples shown in this article, I use an ArrayList and a DataView.
- DataField. This is the field in the parent Datagrid's data source that will correspond to the data to be selected in the dropdown. For example, if the DataSource contains a collection of states, DataField would be something like "StateCode", or whatever the field is named in the table using the states.
- DataTextField. This is the text to be displayed in the dropdown, which may or may not be the same as the underlying value.
- DataValueField. This is the value representing a particular dropdown choice. The DataValueField is often an integer value or other code, while DataTextField is the textual description, which will be more meaningful to the user.
Next override InitializeCell, one of the Datagrid column's inherent events. InitializeCell occurs for every table cell in the column, much like the ItemCreated event when working the Datagrid directly. Here you can manipulate the cell contents, such as setting the HeaderText and adding the DropDownList control you'll be adding your data to. I've also added handlers for the cell's DataBinding event, which will need to be processed differently depending on whether the row is currently being edited or not. Every System.Web.UI.Control has a DataBinding event, and this is where you can get access to the underlying data as the data is being bound to a control, in this case, a TableCell object inside a Datagrid.
Public Overrides Sub InitializeCell(ByVal cell As TableCell, _ ByVal columnIndex As Integer, _ ByVal itemType As ListItemType) MyBase.InitializeCell(cell, columnIndex, itemType) Select Case itemType Case ListItemType.Header cell.Text = HeaderText Case ListItemType.Item, ListItemType.AlternatingItem AddHandler cell.DataBinding, AddressOf ItemDataBinding Case ListItemType.EditItem AddHandler cell.DataBinding, AddressOf EditItemDataBinding Dim DDL As New DropDownList cell.Controls.Add(DDL) End Select End Sub
Next the ItemDataBinding routine, which is fired as each Item or AlternatingItem in the Datagrid is data-bound. Here you need to get a reference back to the TableCell being bound, which you can do as a direct cast of the "sender" object passed in to the event, plus get reference to the current DataGridItem, using the TableCell's NamingContainer property. From there you can just display the contents of the DataField as plain text, just as would be the case were this field displayed in a BoundColumn. Finally, I like to give the user a friendlier error message if the field they specified doesn't exist; otherwise they'd just get the generic "Index was out of range" message.
Private Sub ItemDataBinding(ByVal sender As Object, ByVal e As EventArgs) Dim cell As TableCell = CType(sender, TableCell) Dim DGI As DataGridItem = CType(cell.NamingContainer, DataGridItem) Try cell.Text = DGI.DataItem(DataField) Catch RangeEx As IndexOutOfRangeException Throw New Exception("Specified DataField was not found.") Catch OtherEx As Exception Throw New Exception(OtherEx.InnerException.ToString) End Try End Sub
Next, you will code the EditItemDataBinding event, which will happen to our custom column's cell when a row goes into Edit mode. Again get a reference to the current cell and the DropDownList control inserted during the InitializeCell method. Add a blank item as the first choice in the DropDownList, which will be selected if the data currently in the column does not match up to any of the items you place in the list from the DataSource collection.
Then you will need to determine the type of collection passed in. For the purposes of this example, I will handle 2 cases, a set of strings passed in through an ArrayList, or a DataView from a table of data, which is made up of DataRowView items. For string data, I will enter a new ListItem and set both the value and the text of the dropdown item. The text is all that's really needed here since the two are identical, but I'll choose the appropriate item to select based on the value, in order to accommodate the next example, which sets a separate value property. For DataRowView items, I noted in the earlier example that DataRowViewInstance("FieldName") returns an object representing the data for that field. The same approach can be used to retrieve the values needed for DataTextField and DataValueFields.
Finally, throw a few exceptions to handle common errors that developers could make when using this column, such as sending an invalid field name in the DataField property, or passing in an incompatible DataSource type. I've hard-coded the exception messages to bubble upward, but hopefully in a real application, you would store these somewhere more configurable, such as in your web.config file, or in a resource file if you want to be able to globalize your application. Also, you don't really have to throw that "Specified DataField was not found" exception again here, because it would already have been caught in the ItemDataBinding event, before the Datagrid is ever put into Edit mode.
Private Sub EditItemDataBinding(ByVal sender As Object, _ ByVal e As EventArgs) Dim cell As TableCell = CType(sender, TableCell) Dim DDL As DropDownList = _ CType(cell.Controls(0), DropDownList) Dim DataSourceItem As Object Dim item As ListItem Dim DGI As DataGridItem 'Add a first, blank option DDL.Items.Add(New ListItem("")) For Each DataSourceItem In DataSource Select Case DataSourceItem.GetType.ToString Case "System.String" 'Applies to ArrayList example item = New ListItem(DataSourceItem, DataSourceItem) DDL.Items.Add(item) Case "System.Data.DataRowView" Dim DRV As DataRowView = _ CType(DataSourceItem, DataRowView) item = New_ ListItem(DRV(DataTextField), DRV(DataValueField)) DDL.Items.Add(item) Case Else Throw New Exception("Invalid DataSource type.") End Select Next Try DGI = CType(cell.NamingContainer, DataGridItem) item = DDL.Items.FindByValue(DGI.DataItem(DataField)) Catch RangeEx As IndexOutOfRangeException Throw New Exception("Specified DataField was not found.") Catch OtherEx As Exception Throw New Exception(OtherEx.InnerException.ToString) End Try If Not item Is Nothing Then item.Selected = True End Sub
Now that you've seen all the code necessary to create the DropDownColumn class, let's look at what is required to put this control into use in an application. If you're following along at home, and haven't done so already, go ahead and build the namespace created above into MyCustomColumn.dll, and copy it into the /bin folder of an application where you want to try it out. For my purposes, I created a new Web application called UseCustomColumn, and added a reference to the MyCustomColumn.dll found in my /bin directory. At the top of an ASPX file, add the @Register directive:
<%@ Register TagPrefix="dgg" Namespace="MyCustomColumn" Assembly="MyCustomColumn" %>
Note that the new Datagrid column type won't appear in the Visual Studio .NET Property Builder for the Datagrid, so you'll have to go into the HTML view and add the column declaration there. Make sure the Datagrid declaration is inside a set of <form runat="server">...</form> tags, which is needed to handle the PostBacks. Here's what the rest of the ASPX file looks like:
The Datagrid is bound to the Orders table in the Northwind sample, and the custom DropDownColumn is bound to the ShipVia column. For now I'll only set the DataField property, as I'm just binding to a simple ArrayList, and the DataTextField and DataValueField properties aren't needed. The ArrayList option is simplest when you have a pre-defined list of non-changing data, or you want a quick-and-dirty method to set options. The DataSource for the DropDownColumn is set in code, first getting a reference to the DropDownColumn:
Dim DDC As MyCustomColumn.DropDownColumn DDC = CType(DataGrid1.Columns(4), MyCustomColumn.DropDownColumn) Dim AL As New ArrayList AL.Add("Shipping Company A") AL.Add("Shipping Company B") AL.Add("Shipping Company C") DDC.DataSource = AL
And here is the output when you run this:
Figure 3. Using an ArrayList
Next I need to convert this sample over to use a live table from the database. ShipVia is a foreign key to the lookup table "Shippers," which I'll assign as the DataSource to our DropDownColumn in the code. I also need to alter the declaration of the DropDownColumn to include the DataTextField and DataValueField names that match the corresponding fields in the Shippers table:
<dgg:DropDownColumn DataField="ShipVia" DataTextField="CompanyName" DataValueField="ShipperID" HeaderText="Ship Method" />
Then bind up both the Orders table to the Datagrid, and the Shippers table to the custom column:
Dim SQL As String = "SELECT OrderID, ShipName, ShipCountry, ShipVia FROM Orders" Dim DA As SqlDataAdapter = New SqlDataAdapter(SQL, ConnStr) Dim DS As New DataSet DA.Fill(DS, "Orders") 'Dim Cmd As SqlCommand = New SqlCommand(SQL, Conn) 'Conn.Open() 'DataGrid1.DataSource = _ Cmd.ExecuteReader(CommandBehavior.CloseConnection) DataGrid1.DataSource = DS.Tables("Orders").DefaultView SQL = "SELECT ShipperID, CompanyName " & _ "FROM Shippers ORDER BY ShipperID" DA.SelectCommand.CommandText = SQL DA.Fill(DS, "Shippers") DDC.DataSource = DS.Tables("Shippers").DefaultView DataGrid1.DataBind()
With the live data, the DataGridColumn automatically selects the proper item from the DropDownList based on the value (1, 2, or 3) in the Orders table, like so:
Figure 4. Retrieving data from the database
The last step in using the DropDownColumn is retrieving the selected value to pass back into the update of the database. To do this, simply get a reference to the DropDownList control inside the cell, and determine its SelectedValue property:
Private Sub DataGrid1_UpdateCommand( _ ByVal source As Object, _ ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) _ Handles DataGrid1.UpdateCommand Dim CustomDDL As DropDownList = _ CType(e.Item.Cells(4).Controls(0), DropDownList) Dim NewShip As Integer = CustomDDL.SelectedValue Dim OrderID As Integer = Int32.Parse(e.Item.Cells(1).Text) Dim SQL As String = _ "UPDATE Orders SET ShipVia=@Ship WHERE OrderID=@ID" Dim Conn As SqlConnection = New SqlConnection(ConnStr) Dim Cmd As New SqlCommand(SQL, Conn) Cmd.Parameters.Add(New SqlParameter("@Ship", NewShip)) Cmd.Parameters.Add(New SqlParameter("@ID", OrderID)) Conn.Open() Cmd.ExecuteNonQuery() Conn.Close() DataGrid1.EditItemIndex = -1 BindGrid() End Sub
That wraps up how to create a new column type derived from DataGridColumn, tap into the databinding as it happens, and put the thing into use in a real application. This is but one example of a reusable Datagrid column, and so my challenge to you is to examine your own applications to see what functionality you're repeating that could be neatly wrapped up into its own custom Datagrid column. Develop your own columns either for common-usage scenarios like a DropDownList in a column, or with specialized functionality specific to your organization. You don't have to limit yourself by just including one ASP.NET control inside your custom column either; you can make more complex structures, such as nesting a series of other controls, third-party content, or an entire Datagrid control inside your column to make a hierarchy of information. So let your imagination run wild.
The five built-in column types are pretty useful, and they meet the needs of most display scenarios you'll find in working with the Datagrid control. Without deriving your own control however, you're stuck with putting any of the fancy stuff inside a TemplateColumn, in a free-form fashion. Creating custom columns lets you break free from those constraints, and add rich functionality to your Datagrid applications.
About the Author
Marcie "Datagrid Girl" Robillard, Microsoft MVP for ASP.NET, is an independent consultant and trainer focusing on ASP.NET. Her specialty is the ASP.NET Datagrid, and she has devoted a Web site to it, DatagridGirl.com. On the site you will find links to the best Datagrid articles, books reviewed for Datagrid content, and a growing collection of Datagrid FAQs. Marcie also wastes, err, spends a lot of time moderating the ASP.NET Forums and giving answers wherever Datagrid questions can be found. Marcie's current projects involve mentoring companies as they develop their .NET skills. For specialized consulting or training within your organization, contact Marcie@DatagridGirl.com.
MetaBuilders.com. Free custom Datagrid columns, including source code (C#).
DatagridGirl.com. Resource for all things Datagrid.
ASP.NET Data Web Controls by Scott Mitchell, SAMS, 2003, ISBN 0672325012