How can we show parent/child data, where the header template
represents a parent object and the content panel represents child data related
to that parent? To illustrate, I am going to use the following database structure.
Figure 1

For the accordion, the header for each accordion pane will
be the customer name. Each customer has zero or more orders displayed in the
content template and these orders belong to the customer. These orders will be
displayed below the customer in a GridView list, using the template below.
Listing 2
<ajax:Accordion ID="accComplexData" runat="server">
<HeaderTemplate>
Customer:
<asp:Label ID="lblLastName" runat="server" Text='<%# Eval("LastName") %>' />,
<asp:Label ID="lblFirstName" runat="server" Text='<%# Eval("FirstName") %>' />
</HeaderTemplate>
<ContentTemplate>
Joined on:
<asp:Label ID="lblJoinedDate" runat="server" Text='<%# Eval("CreatedDate") %>' />
<br /><br />
<asp:GridView ID="gvwOrders" runat="server"
DataSource='<%# Eval("Orders") %>' DataKeyNames="OrderKey">
<Columns>
<asp:BoundField HeaderText="Total Amount" DataField="TotalAmount" />
<asp:BoundField HeaderText="Reference" DataField="ReferenceNumber" />
<asp:BoundField HeaderText="Order Date" DataField="OrderDate"
DataFormatString="{0:MM/dd/yyyy}" />
</Columns>
<EmptyDataTemplate>No orders have been submitted.</EmptyDataTemplate>
</asp:GridView>
</ContentTemplate>
</ajax:Accordion>
When the accordion binds, a new accordion pane represents
the Customer record. The Customer object has an Orders property related to that
customer, and is the source for the GridView control. However, the content pane
can still contain information about the customer because both the header and
content template are bound to the Customer object.
If you try to change the data source, such as to refresh the
grid, it may be better to handle binding the grid in the Accordion's
ItemDataBound event handler. This event fires once for each bound item in the
accordion. To access the controls in the row, use the following approach.
Listing 3
void accComplexData_ItemDataBound(object sender,
AjaxControlToolkit.AccordionItemEventArgs e)
{
if (e.ItemType != AjaxControlToolkit.AccordionItemType.Content)
return;
Customer customer = e.Item as Customer;
if (customer == null) return;
GridView grid = e.AccordionItem.FindControl("gvwOrders") as GridView;
if (grid == null) return;
grid.DataSource = customer.Orders;
grid.DataBind();
}
This approach retrieves a reference to the GridView control
via FindControl. Once found, the reference to the Customer object for that
accordion pane is accessible through the Item property. The Customer object
can then have its Orders collection bound to the grid.
The reason this may be better is because if you try to
rebind the grid later individually, you may receive an error when trying to
bind to a different result set manually. I tried leaving both the DataSource
Eval expression in the grid, as well as the code that binds in code-behind, and
the following result is an error: "Databinding methods such as Eval(),
XPath(), and Bind() can only be used in the context of a databound control."
How could the underlying grid be refreshed in cases of
updates occurring elsewhere? There are two options to refresh the grid; the
first option is to rebind the accordion each time. Let us look at a few
approaches. The first approach is to rebind the entire accordion. For this to
work, I have added a refresh button that simply rebinds the grid with all of
the new customer information.
Listing 4
private void BindAccordions()
{
CustomerBAL bal = new CustomerBAL(this.DataContext);
this.accComplexData.DataSource = bal.GetAll();
this.accComplexData.DataBind();
}
As another approach, because each accordion item contains a
grid, it is possible to rebind data to a single grid. To do that, some
information about the key for the customer for the current record needs stored
in that row. What this means is that with each grid there needs to be some way
to track the key for the customer that accordion item represents. I usually use
a hidden field.
Listing 5
<asp:HiddenField ID="hdnCustomerKey" runat="server"
Value='<%# Eval("CustomerKey") %>' />
An alternative approach is to define the CommandArgument
property of the button.
Listing 6
<asp:Button ID="btnRefresh" runat="server" Text="Refresh" CommandName="Refresh"
CommandArgument='<%# Eval("CustomerKey") %>' />
I will use the last approach in the code below; however,
both approaches keep the customer key with the grid. When the refresh button
for each grid is clicked, the key is retrieved from the command argument
property through the button reference. Using the key, the Customer record is
queried from the database using the key and the Orders are bound to the grid's
reference, also retrieved using the FindControl method.
Listing 7
void accComplexData_ItemCommand(object sender, CommandEventArgs e)
{
if (e.CommandName != "Refresh")
return;
GridView grid = (GridView)this.accComplexData
.Panes[this.accComplexData.SelectedIndex].ContentContainer.FindControl("gvwOrders");
CustomerBAL bal = new CustomerBAL(this.DataContext);
Customer customer = bal.GetByKey((Guid)e.CommandArgument);
this.BindGrid(grid, customer);
}
The actual binding of the grid is simple; I always pull
binding information out of the event handler code and use a separate method as
defined below.
Listing 8
private void BindGrid(GridView grid, Customer customer)
{
grid.DataSource = customer.Orders;
grid.DataBind();
}
However, this could use some refactoring. Rather than
retrieving the customer reference in the event handler, it would be better to pass
in the GUID instead. This moves the data access calls into one place, instead
of repeating the same code separately. This has been refactored as follows.
Listing 9
private void BindGrid(GridView grid, Guid customerKey)
{
CustomerBAL bal = new CustomerBAL(this.DataContext);
Customer customer = bal.GetByKey(customerKey);
if (customer == null)
throw new NullReferenceException(string.Format("The customer for {0} is null",
customerKey));
grid.DataSource = customer.Orders;
grid.DataBind();
}
This method can be called with: this.BindGrid(grid,
(Guid)e.CommandArgument); .
This is a little cleaner and the BindGrid method can be used
in multiple situations by passing in the GUID, and the data access code is all
in the same place. I think this is a better approach to take and we will see
that it turns out useful in the following section.