Sometimes we want to create a parent/child report that shows all the records from the child table, organized by parent. I have been thinking about this thing in my head lately but haven’t been sure for long time about how to get it done with web forms.
This implementation is actually so easy with ASP.NET MVC framework.The model on the view has its own mappings to relational tables on the database (assuming that we are using an ORM such as Entity Framework) and a foreach loop will do the trick. So, how is this thing done with ASP.NET web forms? Let’s demonstrate a sample.
I have created a new ASP.NET Web Application under .Net Framework 4 and also my database under this project. (I’m using Visual Studio 2010 as IDE but feel free to use Visual Web Developer Express 2010) After that I created my ADO.NET Entity Data Model with database first approach. Our model should look like as following;
After creating our database structure and ORM (and filling our database tables with some fake values for demonstration purpose), now we’ll be playing with ASP.NET GridView control on a web form page.
The fundamental idea is to create a GridView control for the parent table (this is Category class in our case) that contains an embedded GridView for each row. There child GridView controls are added into the parent GridView using a TemplateField.
But the hard part is that you cannot bind the child GirdView controls at the same time that you bind the parent GirdView because the parent GirdView rows haven’t been created yet. So, we need to wait for GirdView.DataBound event to fire in the parent view before binding the child GridView controls.
In our example, the parent grid view defines two columns and they are both the TemplateField type. The first column combines the category name and category description as you can see below;
<asp:TemplateField HeaderText="Category"> <ItemStyle VerticalAlign="Top" Width="20%" /> <ItemTemplate> <br /> <b><%#Eval("CategoryName")%></b> <br /><br /> <%#Eval("CategoryDescription")%> <br /> </ItemTemplate> </asp:TemplateField>
The second column contains an embedded GridView of products, with two bound columns as you can see below;
<asp:TemplateField HeaderText="Products"> <ItemStyle VerticalAlign="Top" Width="80%" /> <ItemTemplate> <asp:GridView ID="productsGrid" runat="server" AutoGenerateColumns="false"> <Columns> <asp:BoundField DataField="ProductName" HeaderText="Product Name" /> <asp:BoundField DataField="Price" HeaderText="Unit Price" DataFormatString="{0:C}" /> </Columns> </asp:GridView> </ItemTemplate> </asp:TemplateField>
You probably realized that markup for the second GirdView does not set the DataSourceID property. That's because the data source for each of these grids will be supplied programmatically as the parent grid is being bound to its data source.
Now we need to create two data sources, one for retrieving the list of categories and the other for retrieving all products in a specified category. As we have our model as ADO.NET Entity Data Model, we will use EntityDataSoruce to communicate with the database. The following code for first data source which will fill the parent GirdView;
<asp:EntityDataSource ID="EntityDataSource1" runat="server" ConnectionString="name=ProductsEntities" DefaultContainerName="ProductsEntities" EnableFlattening="False" EntitySetName="Categories"> </asp:EntityDataSource>
Now, you need to bind the first grid directly to the data source and your markup for the grid view beginning tag should look like this;
<asp:GridView ID="categoryGrid" AutoGenerateColumns="false" DataKeyNames="CategoryID" DataSourceID="EntityDataSource1" onrowdatabound="categoryGrid_RowDataBound" runat="server" Width="100%">
And here we are on the tricky part; binding the child GirdView controls. First, we need a second EntityDataSource. The second data source contains the query that’s called multiple times to fill the child GridView. Each time, it retrieves the products that are in a different category. The CategoryID is supplied as a parameter;
<asp:EntityDataSource ID="EntityDataSource2" runat="server" ConnectionString="name=ProductsEntities" DefaultContainerName="ProductsEntities" EnableFlattening="False" Where="it.CategoryID = @categoryid" EntitySetName="Products"> <WhereParameters> <asp:Parameter Name="categoryid" Type="Int32" /> </WhereParameters> </asp:EntityDataSource>
To bind the child GridView controls, you need to react to the GridView.RowDataBound event, which fires every time a row is generated and bound to the parent GridView. At this point, you can retrieve the child GridView control from the second column and bind it to the product information by programmatically. To ensure that you show only the products in the current category, you must also retrieve the CategoryID field for the current item and pass it as a parameter. Here’s the code you need;
protected void categoryGrid_RowDataBound(object sender, GridViewRowEventArgs e) { if (e.Row.RowType == DataControlRowType.DataRow) { //get the GridView control in the second column GridView gridChild = (GridView)e.Row.Cells[1].Controls[1]; //set the categoryid parameter so you get the products in the current category only string categoryID = categoryGrid.DataKeys[e.Row.DataItemIndex].Value.ToString(); EntityDataSource2.WhereParameters[0].DefaultValue = categoryID; //Bind the grid gridChild.DataSource = EntityDataSource2; gridChild.DataBind(); } }
Let’s fire up our project and see what happens;
We totally nailed it (I added some style to make it look a little bit better). Perfect.
I hope that you found it useful and it helped