Editing Nested DataGrids in ASP.NET
Recently, while working on a large ASP.NET application, I decided I wanted to repeat data that could be displayed conveniently in a DataGrid. I thought naturally that the DataList was good for repeating things. Why not use the DataList to repeat a DataGrid? I quickly discovered why not. This nesting relationship seems to be broken.
I searched many newsgroups on the Web and discovered that many of you thought that nesting a DataGrid inside of a DataList was a good idea, and there also seems to be some consensus that editing doesn't work correctly. If your experiences are similar to mine, you will have discovered that you can nest a DataGrid in the ItemTemplate of a DataList and switch to edit mode, but the update events don't seem to find their way back to the DataGrid. Well, I have a solution. (Actually, I found two solutions, one which I briefly describe and the other that I will demonstrate.)
If you have been following along in Part 1 and Part 2, you know that you can express some reasonably complex relationships very quickly by using the template sections of a DataList, controlling layout with an HTML table, and using UserControls to implement the template sections of the DataList. In this final installment, I will demonstrate one example and describe a second that will permit you to successfully repeat or nest a DataGrid and get the edit and update behavior to respond correctly. If you are just joining us, I encourage you to read the first two installments of this column in the VB Today section of www.codeguru.com, as I won't be repeating that material here.
Editing Nested Relationships
When you place a DataGrid in the ItemTemplate of a DataList, think of the DataGrid as just another template. The DataGrid is the template that the DataList will repeat for every item in the DataList's data source. Unfortunately, you cannot directly interact with the control used to create the template in the code-behind because it doesn't really exist at runtime; remember, it is a template used to help paint the picture of all rows in the DataList. To alleviate this problem, we can rely on our friend the UserControl.
Place the DataGrid in a UserControl. Add a template column to the DataGrid and add a LinkButton in the DataGrid's ItemTemplate and a LinkButton in the DataGrid's EditItemTemplate. The ItemTemplate button is your edit button, and the EditItemTemplate button is your update button (see Figure 1). To designate the first button as the edit button, set both the CommandName and Text properties to Edit. To designate the second button as the update button, set the CommandName and Text properties to Update. Define an EditCommand and UpdateCommand event handler for the DataGrid. Because the grid is on a UserControl, we can refer to the DataGrid (but not the UserControl) in the code-behind, even when we place the UserControl in the DataList. The two event handlers will look something like those shown in Listing 1.
Figure 1: The template column for a DataGrid, showing how to add an Edit and Update LinkButton.
Listing 1: Examples of the EditCommand and UpdateCommand event handlers.
Private Sub Datagrid2_EditCommand(ByVal source As Object, _ ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) _ Handles Datagrid2.EditCommand Datagrid2.EditItemIndex = e.Item.ItemIndex Datagrid2.DataSource = ACustomer.ContactInformationList Datagrid2.DataBind() End Sub Private Sub Datagrid2_UpdateCommand(ByVal source As Object, _ ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) _ Handles Datagrid2.UpdateCommand ACustomer.ContactInformationList(e.Item.ItemIndex).Number = _ CType(e.Item.Cells(2).Controls(0), TextBox).Text ACustomer.ContactInformationList(e.Item.ItemIndex).Description = _ CType(e.Item.Cells(3).Controls(0), TextBox).Text Datagrid2.EditItemIndex = -1 Datagrid2.DataSource = ACustomer.ContactInformationList Datagrid2.DataBind() End Sub
The basic behavior of the EditCommand event handler is to set the DataGrid's EditItemIndex to the selected row in the DataGrid and rebind the source data. The basic behavior of the UpdateCommand is to read the new data out of the DataGrid's generated template controls, set the EditItemIndex back to -1 (not selected), and rebind the data source. The hardest part about extracting data from the DataGrid is that one must know that the EditItemTemplate creates a textbox for each column in the DataGrid by default. (A TextBox is used for edit mode unless you define a specific template yourself.) Consequently, we need to find the TextBox control relative to each data cell, read the value out that control, and perform any type conversions if necessary. An example of this is indicated in bold font.
After the UserControl is defined with the DataGrid on it, we need to place the UserControl in the ItemTemplate (at a minimum) for our DataList. Finally, we need to bind the data source of the DataList and define the data binding statement for each nested DataGrid.
Recall from Part 2 that the data source we are working with (while it can be any object that implements IEnumerable) is a strongly typed collection of Customer objects, each containing a strongly typed collection of ContactInformation objects. Alternatively, represented as a DataSet, we have many Customers and a one-to-many relationship between Customer objects and ContactInformation objects.
In my implementation, the DataList is bound to the CustomerList (see Listing 2) and each UserControl with a DataGrid is bound to a Customer.ContactInformationList. The binding statement for the UserControl was implemented as HTML block script (see Listing 3), and the code-behind for the UserControl with DataGrid is provided in Listing 4.
Listing 2: Databinding for the DataList occurs in the Page_Load event.
Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load DataList1.DataSource = CustomerList.GetCustomerList DataList1.DataBind() End Sub
Listing 3: Databinding for the nested UserControl with DataGrid was implemented in the HTML as block script.
<uc1:DataGridUserControl id=DataGridUserControl1 runat="server" Data='<%# Container.DataItem %>'></uc1:DataGridUserControl>
Listing 4: The code-behind for the UserControl with DataGrid.
Public Class DataGridUserControl Inherits System.Web.UI.UserControl [ Web Form Designer Generated Code ] Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Datagrid2.DataSource = ACustomer.ContactInformationList Datagrid2.DataBind() End Sub Public Property Data() As Object Get Return ACustomer End Get Set(ByVal Value As Object) SetData(Value) End Set End Property Private ACustomer As Customer = Nothing Public Sub SetData(ByVal data As Object) ACustomer = CType(data, Customer) Datagrid2.DataSource = ACustomer.ContactInformationList Datagrid2.DataBind() End Sub Private Sub Datagrid2_EditCommand(ByVal source As Object, _ ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) _ Handles Datagrid2.EditCommand Datagrid2.EditItemIndex = e.Item.ItemIndex Datagrid2.DataSource = ACustomer.ContactInformationList Datagrid2.DataBind() End Sub Private Sub Datagrid2_UpdateCommand(ByVal source As Object, _ ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) _ Handles Datagrid2.UpdateCommand ACustomer.ContactInformationList(e.Item.ItemIndex).Number = _ CType(e.Item.Cells(2).Controls(0), TextBox).Text ACustomer.ContactInformationList(e.Item.ItemIndex). _ Description = CType(e.Item.Cells(3).Controls(0), _ TextBox).Text Datagrid2.EditItemIndex = -1 Datagrid2.DataSource = ACustomer.ContactInformationList Datagrid2.DataBind() End Sub End Class
Listing 2 shows that we need to bind the DataList each time the page is loaded. In Part 2, we used IsPostBack to reduce the number of times we bound the DataList, but in this example we need to get data all of the way to the each DataGrid and rebinding seems to work best.
Listing 3 demonstrates the HTML script block that binds each Customer object to each dynamically generated UserControl. It is not obvious from the code that Container.DataItem represents a Customer object, but we know that the DataList is bound to a CustomerList. Place the DataGridUserControl in each template where you want it to appear; specifically, you only need the DataGridUserControl in the DataList's ItemTemplate to edit the DataGrid because it is the DataGrid that will be placed in edit mode, not the DataList.
Listing 4 demonstrates the code-behind for the DataGridUserControl. It is the public Data property that the DataList's script block calls in Listing 2. And, while you may be able to reduce the number of times that DataGrid binding occurs, it seems to work best if data binding occurs on Page_Load—which allows the events to fire—and in the actual event—for example, in the UpdateCommand event.
To recap, here are the macro steps summarized:
- Place the DataGrid on a UserControl.
- Define the Edit and Update commands as LinkButtons in a template column in the DataGrid.
- Implement he DataGrid's editing events, such as EditCommand and UpdateCommand.
- Expose a public property that makes it convenient to send data to the dynamically created UserControl.
- Add a DataList to a different Web Form.
- Place the UserControl containing the DataGrid in the DataList's ItemTemplate.
- Define a script block that assigns the Container.Data item to the UserControl's public property that you added for this purpose.
- Each time the DataList's page loads, bind the DataList to your master data source, which will in turn implicitly pump data to each dynamically created UserControl. The dynamically created UserControl's code-behind will take care of binding the detail source to the DataGrid.
(Don't worry; we'll make all of this code available at http://www.softconcepts.com/source. If you are doing this for the first time, it can be confusing.)