Dynamic Nested ASP.Net GridView using AJAX, Webservices and jQuery
Introduction
I haven’t blogged for a while, I have been really swamped at work.
Anyway last week I after using jQuery for a while (for those who don’t know jQuery is awesome, take the time to try it) I had a brilliant idea. After numerous previous attempts at building nested ASP.Net GridViews I thought hang on a minute, once a gridview has been rendered it is normal HTML. Thus its a table with rows(<tr>) and columns(<td>), so all I need to do is somehow get a empty row after every row to act as a place holder(<div></div>) for the nested grid.
RowDataBound
I did a couple of Google searches and found weird and wonderful ways to implement the empty rows but most of them were far to complex for what I needed. Then I found a post (sorry I cant remember the source) with the following code for the RowDataBound event:
Protected Sub GridView_RowDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs)
If e.Row.RowType = DataControlRowType.DataRow Then
Dim tbl As Table = DirectCast(e.Row.Parent, Table)
Dim tr As New GridViewRow(e.Row.RowIndex + 1, -1, DataControlRowType.EmptyDataRow, DataControlRowState.Normal)
Dim tc As New TableCell()
tc.ColumnSpan = e.Row.Cells.Count
Dim div As New HtmlGenericControl("Div")
tc.Controls.Add(div)
tr.Cells.Add(tc)
tbl.Rows.Add(tr)
End If
End Sub
Ok so the empty row/div has been added. Once rendered if found that you can see these gaps between every row and it didn’t look very nice. So I added a CSS attribute like this:
tc.Style.Add("Padding", "0px")
There we go they are now hidden. Now onto the next step, getting another GridView into that empty div. Firstly we need to somehow trigger something to say load data and put the response in the div, thus it was necessary to add a couple more attributes to the rows and the divs.
The Div Attributes
The first line is to give every empty div a unique ID so we can reference it from jQuery. Next I gave it a class name, this is just for display purposes you don’t need this. Lastly I gave it a custom attribute ParentDiv with the ClientID of it parent row, this will be explained a little later.
div.Attributes.Add("ID", "DivGvwRAR_ST_" + e.Row.Cells(3).Text)
div.Attributes.Add("Class", "NestedGrid")
div.Attributes.Add("ParentDiv", e.Row.ClientID)
The GridViewDataRow Attributes
The first line is a class name, this is just for display purposes you don’t need this, next I added a CSS cursor style to show the little hand, again you don’t need this. The next one is where the fun starts, I’ll come back to explain that.
e.Row.Cells(0).CssClass = "expand_list"
e.Row.Style.Add("cursor", "pointer")
e.Row.Attributes.Add("onClick", "javascript:GetSTAM('DivGvwRAR_ST_" + e.Row.Cells(3).Text + "','" + UserSessionVars.UserID.ToString + "','" + e.Row.Cells(3).Text + "')")
WebService
In order for us to get a response I used a webservice that has been exposed to the Javascript by adding a Service reference to the ScriptManager (I think this functionality is only available in .Net 3.5+), you have to add the following line to your class descriptors <System.Web.Script.Services.ScriptService()> for this to work.
<System.Web.Services.WebMethod()> _
Public Function GetAccountManagers(ByVal UserID As Integer, ByVal SalesTeamID As Integer) As String
Return GetRAR_AM(UserID, SalesTeamID)
End Function
Inside GetRAR_AM I create a new GridView and bind it and return the HTML, but I’m not going into those details.
Client Side Javascript and jQuery
Below you will see the Javascript and jQuery code, you will see the function name is GetSTAM and expects 3 parameters, the function call is set-up in the last line in GridViewdataRow attributes. When this function is called it will call the webservice asynchronously. There are 3 extra parameters onComplete, onError and Div. The first to is self explanatory and they will fire on those events, the Div is passed along as a context/id so the onComplete and onError can use that div to update the appropriate div with the response of the webservice. The args parameter is the response from the webservice.
// empty div below account manager
var STDiv = "#none";function GetSTAM(Div, UserID, SalesTeamID) {//check if the prev ST is the same the clicked ST
if (STDiv != ("#" + Div)) {
//empty and minimize the previous div. This is to ensure that only one summary displays at a time
$(STDiv).html("");
$(STDiv).css({ 'padding': '0px' });//get results from web service
RenewalsProcessWS.GetAccountManagers(UserID, SalesTeamID, 1, OnComplete, OnError, Div);//set STDiv = to newly populated div
STDiv = ("#" + Div)
} else {
//empty and minimize the previous div.
$(STDiv).html("");
$(STDiv).css({ 'padding': '0px' });//Reset divs
STDiv = "#none";
}
return (true);
}function OnComplete(args, Div) {
// set fill div
$("#" + Div).html(args);//scroll the newly expanded div to top of screen
if ($("#" + Div).attr("parentdiv") != null)
$("html, body").animate({
scrollTop: $("#" + $("#" + Div).attr("parentdiv")).offset().top - 10
}, 500);
}function OnError(error) {
$(PrevDiv).html("");
$(PrevDiv).css({ 'padding': '0px' });
if (error.get_timedOut())
alert("Time Out Occured: " + error.get_message());
else
alert("Stack Trace: " + error.get_stackTrace() + "/r/n" + "Error: " + error.get_message() + "/r/n" + "Status Code: " + error.get_statusCode() + "/r/n" + "Exception Type: " + error.get_exceptionType() + "/r/n" + "Timed Out: " + error.get_timedOut());
}ConclusionI hope this all makes sense, if not please leave a comment and I will try and assist where I can. You can use the same method above to do n levels of nesting.