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.)
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.
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.