http://www.developer.com/

Back to article

Editing Nested DataGrids in ASP.NET


June 27, 2003

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:

  1. Place the DataGrid on a UserControl.
  2. Define the Edit and Update commands as LinkButtons in a template column in the DataGrid.
  3. Implement he DataGrid's editing events, such as EditCommand and UpdateCommand.
  4. Expose a public property that makes it convenient to send data to the dynamically created UserControl.
  5. Add a DataList to a different Web Form.
  6. Place the UserControl containing the DataGrid in the DataList's ItemTemplate.
  7. Define a script block that assigns the Container.Data item to the UserControl's public property that you added for this purpose.
  8. 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.)

Why Can't I Edit a DataGrid Nested in a DataList?

Great. You have successfully displayed the nested DataList and DataGrid controls (see Figure 2), and when you click Edit, the DataGrid switches to edit mode. However, when you click Update, nothing happens. What could have gone wrong? Unfortunately, I am not sure about the answer to this question. It seems that the DataGrid either is not rewiring the events to each dynamically created UserControl (because the events are part of the UserControl class), or the DataGrid events aren't getting fired, or both. Whatever is happening, or perhaps is not happening, seems to be a bug. In all likelihood, you have an application to get working and don't have time to fix bugs in core Web controls. So, let's talk about a reasonable workaround.



Click here for a larger image.

Figure 2: The DataList showing Customer information and the nested DataGrid showing Contact information for each Customer.

Removing the DataList from the Equation

What does the DataList really do? From where I am sitting, it looks like, for the most part, the DataList repeats controls defined in the template. If the only control we define in the template is a UserControl, it's easy enough for you and me to load all of the UserControls we want and bind them individually. In fact, this seems to be a workable solution to our problem.

If we remove the DataList from the equation and move all of the unique controls to a UserControl, we can load an instance of our UserControl, give it some data, and add it to the page's Controls collection. This doesn't require very much code and seems to create an almost identical visual result, and, most importantly, all events seem to fire.

In Listing 5, I added a TextBox for each of the ID, First Name, Last Name, and Email address to the UserControl using an HTML table to constrain the layout. I also added the DataGrid in cell 4 followed by an image in cell 5. The new UserControl contains one templatized row. The key is to load the UserControl for each Customer object, bind the UserControl to data, and add the dynamically loaded UserControl to the Controls collection of a control on the Web Form, shown in listing 6.

Listing 5: The code-behind for the new UserControl.

Public Class NewControl
  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

    BindData()
      
  End Sub

  Private Sub BindData()
    TextBoxID.Text = ACustomer.ID
    TextBoxFirstName.Text = ACustomer.FirstName
    TextBoxLastName.Text = ACustomer.LastName
    ImagePicture.ImageUrl = ACustomer.Picture
    Datagrid2.DataSource = ACustomer.ContactInformationList
    Datagrid2.DataBind()
  End Sub

  Private ACustomer As Customer

  Public Property Data() As Object
  Get
    Return Data
  End Get
  Set(ByVal Value As Object)
    SetData(Value)
  End Set
  End Property

  Private Sub SetData(ByVal Value As Object)
    ACustomer = CType(Value, Customer)
    Me.BindData()
  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
    BindData()

  End Sub

  Private Sub Datagrid2_UpdateCommand(ByVal source As Object, _
    ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) _
    Handles Datagrid2.UpdateCommand

    ACustomer.ID = TextBoxID.Text
    ACustomer.FirstName = TextBoxFirstName.Text
    ACustomer.LastName = TextBoxLastName.Text
    ACustomer.Email = TextBoxEmail.Text

    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
    BindData()

  End Sub
End Class

Listing 6: The code-behind that dynamically loads a UserControl for each object in my master data source.

Private Sub Page_Load(ByVal sender As System.Object, _
  ByVal e As System.EventArgs) Handles MyBase.Load

  Dim ACustomer As Customer

  For Each ACustomer In CustomerList.GetCustomerList
    Dim MyControl As Control = LoadControl("NewControl.ascx")
    CType(MyControl, NewControl).Data = ACustomer
    Target.Controls.Add(MyControl)
    Target.Controls.Add(New LiteralControl("<BR>"))
  Next

End Sub

Listing 5 shows that each time the UserControl gets data or its edit state changes, we rebind to all of the controls in the UserControl. The one piece left out is that we may have to devise a scheme for making the TextBox controls, such as that used to store the ID, FirstName, LastName, and Email address read only when we aren't in edit mode and writable when we are in edit mode; as an alternative, you could permit editing of the main data—a separate step from editing the grid. I will leave it for you to decide what works best in your implementation.

Listing 6 is the code that replaces the DataList. In Listing 6, Target is the name of an HtmlTableCell whose runat property is equal to server. A table cell makes a convenient place to load dynamic controls. (You also might use a control such as the PlaceHolder for this purpose.) The trick is to iterate over each object in the data source, load the UserControl, hand off the data to the UserControl, and add the UserControl to the Container's Controls collection. After finishing these few simple steps, the nested DataGrid will move easily into and out of edit mode and all events will fire correctly.

The sample in Listings 5 and 6 addresses and resolves the same functional behavior as the earlier nested DataList and DataGrid example. What isn't demonstrated is how to replicate the same visual appearance. Massaging the appearance is simply a matter of adding a style sheet and changing control properties until you get the result you desire.

The biggest drawback to not using the DataList is that you do not automatically get the different styles for item, selected item, and edit item templates. You will have to apply different styles to each of the dynamically loaded UserControls to simulate the styles associated with the various edit regions in the DataList, but this is comparatively easy to do.

Alternate Scenario

If you have your heart set on using a DataList with a nested DataGrid, my good friend Joe Shook demonstrated a functional way to do this as well.

Define a Web Form with a DataList. In that form, implement your editing events. Each time the DataList.ItemCreated event fires, the ItemCreated event will send an argument containing a DataListItem. The DataListItem contains the dynamically created UserControl. Using the event argument, call e.Item.Find("uc1"), where uc1 is your UserControl's ID, to obtain a reference to the template-created UserControl. If the Find returns the UserControl, request the DataGrid on that UserControl and manually re-associate your edit event handlers with the DataGrid by calling AddHandler.

In this alternative example, your UserControl and DataGrid's events actually reside in the page containing the DataList. I am not a big fan of that part of the solution, but it does seem to work. A modest revision would entail moving the edit event handlers to the UserControl and binding the DataGrid's event properties to the UserControl's event handlers using AddHandler. This revision should work and now your behavior travels with your component, the UserControl.

Summary

There are so many things I like about programming in .NET that the few things that seem out of whack don't bother me too much. From my own experience, conversations, and newsgroups, it appears that nesting a DataGrid in a DataList doesn't work quite the way one would expect. Perhaps this is by design or is a bug; we may never know.

Through experimentation, it appears that a nested DataGrid's edit events aren't restored when the DataGrid is nested in a DataList. In this article, Part 3 of 3, I demonstrated a workaround for the nonexistent edit behavior that excludes the DataList from the equation, requiring you to load a UserControl containing a DataGrid for each object in the master data source—this is the data used as the DataList's data source. As an alternative, you can use the DataList with a nested DataGrid, but you will have to manually find the dynamically created DataGrids defined in the DataList's templates and re-associate the DataGrid event handlers in the DataList's ItemCreated event. The later approach, while not shown here, has been demonstrated to work.

I suspect from conversations I have had that Microsoft is aware of this relatively minor problem and a more permanent solution will be forthcoming.

About the Author

Paul Kimmel is a software architect, writer, and columnist for codeguru.com. Look for his recent book Visual Basic .NET Power Coding from Addison-Wesley. Paul Kimmel is available to help design and build your .NET solutions and can be contacted at pkimmel@softconcepts.com.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date