Introduction
The Accordion control is a flexible control that neatly separates region of the user interface and condenses the overall user interface.
This control can support data binding, which I personally didn't know until recently. The Accordion control supports binding data to the
Header and Content templates, and we'll explore how to setup some more complex interfaces some of these scenarios in this article.
Header/Content Template
Within the accordion control, it's possible to bind data to the header and content templates. However, the header and content template
have a one-to-one relationship, meaning that one header template matches one content template. Let me illustrate this; look at the following
example, which binds Customer records to an accordion:
<ajax:Accordion id="accSingleData" runat="server">
<HeaderTemplate>
<asp:Label ID="lblLastName" runat="server" Text='<%# Eval("LastName") %>' />
</HeaderTemplate>
<ContentTemplate>
<asp:Label ID="lblFirstName" runat="server" Text='<%# Eval("FirstName") %>' />
</ContentTemplate>
</ajax:Accordion>
If you expect this example to group entries by last name, it won't; rather, a pane is created for each customer, with the last name as the header, and the first name as the content. It's a one-to-one relationship.
Parent/Child Data
How can we show parent/child data, where the header template represents about a parent object, and the content panel represents child
date related to that parent? To illustrate, I'm going to use the following database structure illustrated in a screenshot below:
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, which these orders belong to the customer. These orders will be displayed below the customer in a GridView list,
using the template below:
<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:
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 everytime. Let's look at a few approaches. The first approach is to rebind the entire accordian.
For this to work, I've added a refresh button that simply rebinds the grid with all of the new customer information.
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's 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:
An alternative approach is to define the CommandArgument property of the button as follows:
<asp:Button ID="btnRefresh" runat="server" Text="Refresh" CommandName="Refresh"
CommandArgument='<%# Eval("CustomerKey") %>' />
I'll 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:
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 defined
as:
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 removes the data access calls into one place, instead of repeating the same code separately. This has been
refactored as follows:
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'll see that it turns out useful in the following section.
Grid Selection
To setup selection for the grid, I changed the grid definition to the following:
<asp:GridView … AutoGenerateSelectButton="true"
OnSelectedIndexChanging="gvwOrders_SelectedIndexChanging">
To select the grid when the grid is bound manually, the SelectedIndexChanging event needs handled so that the grid can be rebound to show
the current selected item.
protected void gvwOrders_SelectedIndexChanging(object sender, GridViewSelectEventArgs e)
{
GridView grid = (GridView)sender;
HiddenField customerKeyField = (HiddenField)grid.FindControl("hdnCustomerKey");
this.BindGrid(grid, new Guid(customerKeyField.Value));
}
Using the BindGrid refactoring, passing in the guid works well to incorporate two scenarios. However, you don't really need to handle
the selection approach from the grid (in a manual bind scenario).
Grid Editing
The gridview fires the RowEditing event whenever the mode changes from read-only to displaying an edit interface. To invoke editing, click
a button with the CommandName set to Edit. The grid needs three things: the AutoGenerateEditButton property needs set to true, the
RowEditing event needs an event handler attached to it, and the grid needs rebound with the EditIndex property set to the current row
(before the data bind). Take a look at the approach below. This event is wired up to each grid in the accordion.
protected void gvwOrders_RowEditing(object sender, GridViewEditEventArgs e)
{
GridView grid = (GridView)sender;
grid.EditIndex = e.NewEditIndex;
HiddenField customerKeyField = (HiddenField)grid.FindControl("hdnCustomerKey");
this.BindGrid(grid, new Guid(customerKeyField.Value));
}
Notice that the method pulls the key from the hidden field setup in the accordion template, and uses the same BindGrid method to repopulate
the grid.
Update works in a similar manner; any updates need to be performed manually, as shown below:
protected void gvwOrders_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
GridView grid = (GridView)sender;
GridViewRow row = grid.Rows[e.RowIndex];
string totalAmount = ((ITextControl)row.Cells[1].Controls[0]).Text;
string referenceNumber = ((ITextControl)row.Cells[2].Controls[0]).Text;
string createdDate = ((ITextControl)row.Cells[3].Controls[0]).Text;
//Update the record
}
Each field to update is extracted by retrieving the value from the textboxes that hold the value. I didn't show it above, but the
values pulled back from the textboxes are passed to the customer record for the selected customer. This record is then submitted to
the database.
Conclusion
The accordion is handy to create a specific application design. It also supports data binding using the approaches above.