I thought I would share what took me embarrassingly long to figure out on my own, how to add paging to a nested grid view using a DataSourceControl.  Specifically, I am targeting LinqDataSource because I am encapsulating my data with a combination of entity objects and a data context / repository.  In fact, because I am bound to an Oracle database, I don't have Linq to Sql at my disposal.  Actually Linq to Oracle can be done with a little help from the Entity Framework Beta 3 and Core Lab's OraDirect.Net suite of tools.

What follows is the process I followed (as best as I recall) to render nested grid views with paging on the inner grid.

Preconditions

I have well defined entity objects with relationships as opposed to reference ids.    I also have a way to retrieve the List<> of entity objects, via a controller (MVC).  Don't judge the my implementation of MVC.  That is not the point of this article. 

Here is the abbreviated source for the entity objects.  Notice how the Job has a List of JobLineItem and JobLineItem has a reference to Job instead of the job id.  This is typical of the Entiy classes that are generated when you include their relationships in the model.

    public class Job
{
public int Id { get; set; }
public string Status { get; set; }
public List<JobLineItem> LineItems { get; private set; }

public Job()
{
LineItems = new List<JobLineItem>();
}
}

public class JobLineItem
{
public int Id { get; set; }
public string Description { get; set; }
public string Status { get; set; }
public Job Job { get; set; }
}

Define a LinqDataSource For the Master Data

I decided to use a handler for the Selecting event instead of setting the ContextTypeName and TableName properties.  Even if I had used those two properties, I would have been forced to handle the ContextCreating event.  The reason behind all of this is because, as stated above, I am using a method on my controller to get the list of Job objects.

<asp:LinqDataSource ID="JobDataSource" runat="server" OnSelecting="JobDataSource_Selecting" />
    protected void JobDataSource_Selecting(object sender, LinqDataSourceSelectEventArgs e)
{
e.Result = Jobs;
}

 private List<Job> Jobs
{
get
{
List<Job> jobs = (List<Job>)Cache["AllJobs"];
if (jobs == null)
{
jobs = _jobController.ListJobs().Jobs;
Cache["AllJobs"] = jobs;
}
return jobs;
}
}

Build Up the Outer GridView

This is pretty straight forward.  Instead of using the DataSource property, we use the DataSourceID to reference the LinqDataSource.  You should notice that the DataField attributes are set to properties on my Job class.
 <asp:GridView ID="JobGrid" runat="server" DataSourceID="JobDataSource"
 HeaderStyle-CssClass="tableHeader" RowStyle-CssClass="normalRow" AlternatingRowStyle-CssClass="alternateRow"
     AutoGenerateColumns="false" CellPadding="4" >
     <Columns>
         <asp:BoundField HeaderText="Job Id" DataField="Id" />
         <asp:BoundField HeaderText="Status" DataField="Status" />
     </Columns>
</asp:GridView>

Add the Nested GridView

Next we add a TemplateField and the nested GridView.  Notice that I used the binding container to identify the Job for the current row.  Then I simply access the LineItems property which is of type List<JobLineItems>.  Well, I say simply, but it turns out that this didn't work well for me.  When handling the paging, I discovered that it was quite difficult to bind the data again because the parent binding container didn't have the Job instance on postback.  This annoyed me, but it makes a lot of sense.  The outer grid doesn't need to rebind the data because what it is displaying is not changing.  I kept getting a null reference exception whenever I tried to rebind the child because the Job instance wasn't there.
 <asp:GridView ID="JobGrid" runat="server" DataSourceID="JobDataSource"
 HeaderStyle-CssClass="tableHeader" RowStyle-CssClass="normalRow" AlternatingRowStyle-CssClass="alternateRow"
     AutoGenerateColumns="false" CellPadding="4" >
     <Columns>
         <asp:BoundField HeaderText="Job Id" DataField="Id" />
         <asp:BoundField HeaderText="Status" DataField="Status" />
<asp:TemplateField HeaderText="Line Items" >
<ItemTemplate>
<asp:GridView ID="LineItemGrid" runat="server" RowStyle-CssClass="normalRow" AlternatingRowStyle-CssClass="alternateRow"
DataSource="<%#((Job)Container.DataItem).LineItems %>" AutoGenerateColumns="false" ShowHeader="false"
 AllowPaging="true" PageSize="5" OnPageIndexChanging="LineItemGrid_PageIndexChanging" >

<Columns>
<asp:BoundField DataField="Status" ItemStyle-Width="200" />
<asp:BoundField DataField="Description" ItemStyle-Width="300" />
</Columns>
</asp:GridView>
</ItemTemplate>
</asp:TemplateField>
     </Columns>
</asp:GridView>

Using a LinqDataSource For the Nested GridView

I decided to try to use a LinqDataSource for the nested GridView.   The most immediate benefit would be to use the AutoPage attribute on the data source.  This would mean that I would not have to handle the PageIndexChanging event anymore, which is where I was getting my null reference exception.  However, this took me a long time to figure out how to make it work.  It finally payed off in the end.  I tried various events and none of them seemed to fire when I would have liked them to.  It turns out that all of the data binding type of events fired after the Selecting event of the LinqDataSource.  The order is below
  1. Nested LinqDataSource.Selecting   (sender is LinqDataSourceView)
  2. Nested LinqDataSource.ContextCreating   (sender is LinqDataSourceView)
  3. Nested GridView.DataBinding
  4. Master GridView.RowDataBound
Of course, the more I think about it, the more the order of these events make sense.

Regardless, what I was attempting to accomplish was identify which row (by index) was associated with the nested LinqDataSource.  By the way, the DataSourceControl is data-bound, so there will be a new instance of the nested LinqDataSource for each row in the master grid.  Part of the problem as you can see from the list above is that the Selecting and ContextCreating events pass a LinqDataSourceView as the sender, not the LinqDataSource.  This was quite annoying.  If I had the data source, I could just access it's container (row) and determine the row index.  As it turns out, I too quickly forgot the page life cycle.  The Load event came to my rescue:
  1. Nested LinqDataSource.Load   (sender is LinqDataSource)
  2. Nested LinqDataSource.Selecting   (sender is LinqDataSourceView)
  3. Nested LinqDataSource.ContextCreating   (sender is LinqDataSourceView)
  4. Nested GridView.DataBinding
  5. Master GridView.RowDataBound
Here is the updated aspx with the nested LinqDataSource
 <asp:GridView ID="JobGrid" runat="server" DataSourceID="JobDataSource"
 HeaderStyle-CssClass="tableHeader" RowStyle-CssClass="normalRow" AlternatingRowStyle-CssClass="alternateRow"
     AutoGenerateColumns="false" CellPadding="4" >
     <Columns>
         <asp:BoundField HeaderText="Job Id" DataField="Id" />
         <asp:BoundField HeaderText="Status" DataField="Status" />
<asp:TemplateField HeaderText="Line Items" >
<ItemTemplate>
<asp:LinqDataSource ID="LineItemDataSource" runat="server" AutoPage="true"
OnSelecting="LineItemDataSource_Selecting" OnLoad="LineItemDataSource_Load" />

<asp:GridView ID="LineItemGrid" runat="server" RowStyle-CssClass="normalRow" AlternatingRowStyle-CssClass="alternateRow"
AutoGenerateColumns="false" ShowHeader="false" AllowPaging="true" PageSize="5"
DataSourceID="LineItemDataSource"
DataSource="<%#((Job)Container.DataItem).LineItems %>" OnPageIndexChanging="LineItemGrid_PageIndexChanging" >
<Columns>
<asp:BoundField DataField="Status" ItemStyle-Width="200" />
<asp:BoundField DataField="Description" ItemStyle-Width="300" />
</Columns>
</asp:GridView>
</ItemTemplate>
</asp:TemplateField>
     </Columns>
</asp:GridView>

And the code behind:

    protected void LineItemDataSource_Load(object sender, EventArgs e)
{
LinqDataSource ds = (LinqDataSource)sender;
GridViewRow parentRow = (GridViewRow)ds.BindingContainer;
ds.SelectParameters["rowIndex"] = new Parameter
{
Name = "rowIndex",
DefaultValue = parentRow.RowIndex.ToString()
};
}

protected void LineItemDataSource_Selecting(object sender, LinqDataSourceSelectEventArgs e)
{
int rowIndex = Convert.ToInt32(e.SelectParameters["rowIndex"]);
e.Result = Jobs[rowIndex].LineItems;
}

Final Result



Parting Thoughts

A few points should be made here.  I have the luxury of loading all of the data for the page into cache.  Some of you may not be so fortunate.  The data for this page is relatively small.  We are talking about 20 or so jobs with an average of three or four line items each. 

I could have chosen to use a SqlDataSource if I were comfortable accessing Sql directly from my page.  This would have been a reasonable route if the page contained a lot of data.  Of course, Linq to Sql would be even better.  Keep in mind, Linq to Sql goes to the database each time it needs a new page of data.

You might have asked, what if I have paging on the outer grid view as well?  To support this, I only needed to add a couple of lines to the LineItemDataSource_Load event handler:
    protected void LineItemDataSource_Load(object sender, EventArgs e)
{
int offset = JobGrid.PageSize * JobGrid.PageIndex;
LinqDataSource ds = (LinqDataSource)sender;
GridViewRow parentRow = (GridViewRow)ds.BindingContainer;
ds.SelectParameters["rowIndex"] = new Parameter
{
Name = "rowIndex",
DefaultValue = (parentRow.RowIndex + offset).ToString()
};
}


I hope this helps someone.  At the very least, I may come back here in six months just as a reminder.

Cheers,
Will

posted on Saturday, March 1, 2008 8:26 PM

Comments

Gravatar
# re: Nested GridView With Paging
posted by Pushkar
on 7/14/2008 5:19 AM
Hii...
Thanks for the nice post...
Can i have example for the Nested GridView with pagination in ASP.NET 2.0 ?
Gravatar
# re: Nested GridView With Paging
posted by Will Smith
on 7/15/2008 10:10 AM
@Pushkar: I don't have an example from ASP.Net 2.0. It is not likely I can whip one up for you in the near future either. I am pretty busy with a deployment at the moment.

Post A Comment
Title:
Name:
Email:
Comment:
Verification: