The accordion control is a great control to add both effect and functionality to your ASP.NET Ajax enabled site. It presents data in a sleek way and allows for good use of screen space only showing you what you really need to see in a parent child fashion.
Today I am going to demonstrate the ability of nesting gridviews within an accordion control. This will allow you to map and display 1..* relationships in an efficient and logical way. In future posts I will also be showing how to extend this to use the modal popup control to allow easy editing of the data.
The example I am using is a simple books catalog database, where books are mapped to their related category. Here is the SQL for generating the database table structure;
DROP TABLE Book;
DROP TABLE BookCategory;
CREATE TABLE BookCategory (
id INT IDENTITY(1,1) PRIMARY KEY,
category_name VARCHAR(25) NOT NULL
);
CREATE TABLE Book (
id INT IDENTITY(1,1) PRIMARY KEY,
title VARCHAR(50) NOT NULL,
publisher VARCHAR(25) NOT NULL,
ISBN VARCHAR(10) NOT NULL,
category_id INT REFERENCES BookCategory(id)
);
Assuming you have the AJAX ASP.NET Control Toolkit installed the first thing will be too add an accordion control to your page. Define both a header and content section within the accordion. These templates will allow you to customize how the data is displayed. The header will represent our parent record details and when expanded the gridview will present the many rows associated to it.
<asp:ScriptManager ID="ScriptManager" runat="server">
</asp:ScriptManager>
<ajaxToolkit:Accordion
ID="accordionBooks"
runat="server"
SelectedIndex="0"
HeaderCssClass="accordionHeader"
HeaderSelectedCssClass="accordionHeaderSelected"
ContentCssClass="accordionContent"
FadeTransitions="false"
FramesPerSecond="40"
TransitionDuration="250"
AutoSize="None"
RequireOpenedPane="false"
SuppressHeaderPostbacks="true"
onitemdatabound="accordionBooks_ItemDataBound">
<HeaderTemplate>
<p>
<asp:Literal ID="litCategory" runat="server" Text="Category:" />
<asp:Label ID="lblBookCategory" Text='<%# DataBinder.Eval(Container.DataItem, "category_name") %>' runat="server" />
</p>
</HeaderTemplate>
<ContentTemplate>
<asp:HiddenField ID="hdnCategoryId" Value='<%# DataBinder.Eval(Container.DataItem, "id") %>' runat="server" />
<asp:GridView
ID="gvBooks"
runat="server"
DataKeyNames="Id"
AutoGenerateColumns="false"
Width="100%">
<Columns>
<asp:BoundField DataField="id" runat="server" />
<asp:TemplateField>
<ItemTemplate>
<asp:Label ID="lblTitle" Text='<%# DataBinder.Eval(Container.DataItem, "Title") %>' runat="server" />
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField>
<ItemTemplate>
<asp:Label ID="lblPublisher" Text='<%# DataBinder.Eval(Container.DataItem, "Publisher") %>' runat="server" />
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField>
<ItemTemplate>
<asp:Label ID="lblISBN" Text='<%# DataBinder.Eval(Container.DataItem, "ISBN") %>' runat="server" />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
</ContentTemplate>
</ajaxToolkit:Accordion>
Above you can see that we bind our category name to our header template. The content template then contains the gridview which contains the details of each book.
Essentially we will be binding our data in two steps. On page load we will bind our accordion to the book categories from our ‘BookCategory’ table. The category name will be assigned to the label within the header template. The Id of the category will be stored in a hidden field within the content template. This will allow us to get the id needed to be able to bind our books gridview by category type later in the ‘item data bound’ event.
protected void Page_Load(object sender, EventArgs e)
{
try
{
accordionBooks.DataSource = BookDB.GetAllBookCategories().Tables[0].DefaultView;
accordionBooks.DataBind();
}
catch
{
DisplayError("Error while getting data");
}
}
Note that we are using the default view here. While writing this post I forgot this step and couldn’t figure out why I was getting no data showing up. You cannot assign the data source a data table or dataset, however generic list collections seem to bind fine.
The second step to binding is using the the item data bound event, which allows us to bind our data to our gridview as we move through each accordion item. We need to perform a check for item type within this function because it is executed for both header and content sections. We obviously want the content section. We also use the AccordionItemEventArgs to get the current accordion item so that we can find the gridview and hidden field controls and reference them specifically. We then extract the id, perform a database query and bind the gridview with the books related to that category.
protected void accordionBooks_ItemDataBound(object sender, AjaxControlToolkit.AccordionItemEventArgs e)
{
try
{
if (e.ItemType == AjaxControlToolkit.AccordionItemType.Content)
{
GridView gdvBooks = (GridView)e.AccordionItem.FindControl("gvBooks");
HiddenField hdnCategoryId = (HiddenField)e.AccordionItem.FindControl("hdnCategoryId");
int categoryId;
if (!int.TryParse(hdnCategoryId.Value, out categoryId))
throw new Exception("Unable to parse id");
gdvBooks.DataSource = BookDB.GetAllBooksByCategoryId(categoryId);
gdvBooks.DataBind();
}
}
catch
{
DisplayError("Error while getting data");
}
}
And that is all that is there is to it. I have included my database code below for those who are interested as well as the stored procedures.
public static DataSet GetAllBooksByCategoryId(int categoryId)
{
DataSet ds = null;
try
{
using (SqlConnection conn = new SqlConnection("Data Source=LEAF-DEV\\SQLEXPRESS;Initial Catalog=BookCatalog;Integrated Security=SSPI;"))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand("spGetBooksByCategory", conn))
{
cmd.Parameters.Add("@category_id", SqlDbType.Int).Value = categoryId;
cmd.CommandType = CommandType.StoredProcedure;
using (SqlDataAdapter da = new SqlDataAdapter(cmd))
{
ds = new DataSet();
da.Fill(ds);
}
}
}
}
catch(Exception ex)
{
throw ex;
}
return ds;
}
public static DataSet GetAllBookCategories()
{
DataSet ds = null;
try
{
using (SqlConnection conn = new SqlConnection("Data Source=LEAF-DEV\\SQLEXPRESS;Initial Catalog=BookCatalog;Integrated Security=SSPI;"))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand("spGetBookCategories", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
using (SqlDataAdapter da = new SqlDataAdapter(cmd))
{
ds = new DataSet();
da.Fill(ds);
int rowCount = ds.Tables[0].Rows.Count;
}
}
}
}
catch (Exception ex)
{
throw ex;
}
return ds;
}
USE BookCatalog;
DROP PROCEDURE spGetBookCategories;
DROP PROCEDURE spGetBooksByCategory;
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE spGetBookCategories
AS
BEGIN
SELECT category_name, id
FROM BookCategory
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE spGetBooksByCategory(@category_id INT)
AS
BEGIN
SELECT
id,
title,
publisher,
ISBN,
category_id
FROM Book
WHERE category_id = @category_id
END
GO