GridView Grouping Master/Detail Drill Down using AJAX and jQuery
Introduction:
Last month I posted about Building a grouping Grid with GridView and JQuery. And I got feedbacks about how to do the same thing using AJAX (on demand retrieving of detail data). In fact I was thinking of that too, and I had couple of ideas in mind. One of them it to use nested update panels, or use AJAX Data Controls with page method/web service method calls along with ASP.NET AJAX. You can view the [demo here].
I didn't like the nested update panel idea, although it is the easiest I think. And was started to think about the second idea but I was about to check another way other than ASP.NET AJAX as long as I'm using jQuery. So I was thinking of using jQuery AJAX. That was just after reading Dave Ward's post Using jQuery to consume ASP.NET JSON Web Services.
Prerequisites:
I recommend to read Dave's post Using jQuery to consume ASP.NET JSON Web Services before you proceed. And not only the post but also the comments, because the post itself contains the core information while the comments contain issues with resolutions. And myself I used one of the techniques discussed on the post's comments which is Page Method calls using jQuery AJAX. Another post I wish you to read also from Dave's blogwhich is Why do ASP.NET AJAX page methods have to be static?. Also Dave posted about Using jQuery to directly call ASP.NET AJAX page methods. You may wish to review the above posts first.
Implementation:
I modified the sample I provided in my post Building a grouping Grid with GridView and jQuery to apply the new technique I provide here. Simply when the user click on the master (Customer name) the details (Customer Orders) are populated on demaned and displayed underneath on a sliding DIV using jQuery.
I didn't build web service to retrieve the data instead I used a Page Method. Also I used a technique Dave used in his sample. So I'll start from this point. I'll explore the Page Method along with the technique used to retrieve the data.
I built a User Control that is responsible for retireving and displaying the detail data (Customer Order). The user control only contain a SqlDataSource and Repeater control:
1: <asp:SqlDataSource ID="sqlDsOrders" runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>"
2: SelectCommand="SELECT [OrderID], [OrderDate], [RequiredDate], [Freight], [ShippedDate] FROM [Orders] WHERE ([CustomerID] = @CustomerID)">
3: <SelectParameters>
4: <asp:Parameter Name="CustomerID" Type="String" DefaultValue="" />
5: </SelectParameters>
6: </asp:SqlDataSource>
7: <asp:Repeater ID="List" DataSourceID="sqlDsOrders" runat="server">
8: <HeaderTemplate>
9: <table class="grid" cellspacing="0" rules="all" border="1" style="border-collapse: collapse;">
10: <tr>
11: <th scope="col"> </th>
12: <th scope="col">Order ID</th>
13: <th scope="col">Date Ordered</th>
14: <th scope="col">Date Required</th>
15: <th scope="col" style="text-align: right;">Freight</th>
16: <th scope="col">Date Shipped</th>
17: </tr>
18: </HeaderTemplate>
19: <ItemTemplate>
20: <tr class='<%# (Container.ItemIndex%2==0) ? "row" : "altrow" %>'>
21: <td class="rownum"><%#Container.ItemIndex+1 %></td>
22: <td style="width: 80px;"><%Eval("OrderID") %></td> 23: <td style="width: 100px;"><%Eval("OrderDate","{0:dd/MM/yyyy}") %></td> 24: <td style="width: 110px;"><%Eval("RequiredDate", "{0:dd/MM/yyyy}")%></td> 25: <td style="width: 50px; text-align: right;"><%# Eval("Freight","{0:F2}") %></td> 26: <td style="width: 100px;"><%# Eval("ShippedDate", "{0:dd/MM/yyyy}")%></td> 27: </tr>
28: </ItemTemplate>
29: <FooterTemplate>
30: </table>
31: </FooterTemplate>
32: </asp:Repeater>
Below is the OnLoad event handler of the User Control:
1: protected override void OnLoad(EventArgs e)
2: { 3: this.sqlDsOrders.SelectParameters["CustomerID"].DefaultValue = this.CustomerId;
4: base.OnLoad(e);
5: }
I didn't use GridView instead of Repeater because it produced an exception and I didn't investigate much around this issue. Also I'm still using VS.NET 2005 & .Net 2.0 so I didn't yet switch to VS.NET 2008 to use the new features.
As you might notice, the User Control has a property called CustomerId. During the call of the page method, I pass CustomerId to the called mathod which in turn set this property. Now to make the idea fully complete you need to view the Page Method:
1: [System.Web.Services.WebMethod()]
2: public static string GetOrders(string customerId)
3: { 4: System.Threading.Thread.Sleep(500);
5: Page page = new Page();
6: CustomerOrders ctl = (CustomerOrders)page.LoadControl("~/CustomerOrders.ascx"); 7: ctl.CustomerId = customerId;
8: page.Controls.Add(ctl);
9: System.IO.StringWriter writer = new System.IO.StringWriter();
10: HttpContext.Current.Server.Execute(page, writer, false);
11: string output = writer.ToString();
12: writer.Close();
13: return output;
14: }
The above code is exactly taken from David's Sample, I just adjust it to suite my requirements of course like passing and setting CustomerId. Simply create new Page Class (IHttpHandler), load the user control add to the page and finally Execuse the page using Server.Execute. This way I didn't need to spend much time prepare the way to display my data because I already have my HTML ready for render.
That was all about the Server Side code. Its time to explore the client side and how AJAX call is initiated. Each item of the Master GridView (Customers) is displayed like this:
1: <div class="group" style="display:inline" id='<%#String.Format("customer{0}",Container.DataItemIndex) %>' 2: onclick='showhide(<%#String.Format("\"#customer{0}\"",Container.DataItemIndex) %>, 3: <%#String.Format("\"#order{0}\"",Container.DataItemIndex) %>, 4: <%#String.Format("\"{0}\"",Eval("CustomerID")) %>)'> 5: <asp:Image ID="imgCollapsible" CssClass="first" ImageUrl="~/Assets/img/plus.png"
6: Style="margin-right: 5px;" runat="server" /><span class="header">
7: <%#Eval("CustomerID")%>: 8: <%#Eval("CompanyName")%>(<%#Eval("TotalOrders")%> Orders) </span> 9: </div>
10: <div id='<%#String.Format("order{0}",Container.DataItemIndex) %>' class="order"></div>
The first DIV is for Master Item (Customer), second DIV is for Detail Items (Orders). When user click on the first DIV, AJAX request is initiated and returned HTML is set for the second DIV then its slided down displaying the detail items.
The first DIV has onclick client event handler called showhide(div1Id,div2Id,customerId). The handler Initiate the AJAX Request and like the following:
1: $.ajax({ 2: type: "POST", //POST
3: url: "GridViewDrillDownjQueryQAjax.aspx/GetOrders", //Set call to Page Method
4: data: params, // Set Method Params
5: beforeSend: function(xhr) { 6: xhr.setRequestHeader("Content-type", "application/json; charset=utf-8");}, 7: contentType: "application/json; charset=utf-8", //Set Content-Type
8: dataType: "json", // Set return Data Type
9: success: function(msg, status) { 10: $('#progress').css('visibility','hidden'); 11: $(master).children()[0].src = src;
12: $(detail).html(msg);
13: $(detail).slideToggle("normal"); // Succes Callback 14: },
15: error: function(xhr,msg,e){ 16: alert(msg);//Error Callback
17: }
18: });
Because I'm using the same technique explained in Dave's post, there is no much to mention here. However I'm going to explore each parameter passed to the ajax method of jQuery:
- type: type of the request. I use POST method. return to David's post for more details.
- url: URL of the distination I issue a request for (Page or Web Service) attached to is method name I want to invoke.
- data: Data to be sent to the server: '{customerId:"ALFKI"}'. You can also review comments on Dave's post regarding this option or review the documentation.
- beforeSend: Return to Dave's post for more details or to the jQuery.ajax documentation.
- contentType: When sending data to the server, use this content-type. Default is "application/x-www-form-urlencoded", which is fine for most cases. I recommend that you checkout Dave's post's comments as it contains resolution for an issue related to IE. As a summary, we use beforeSend to set the content type of the request, for some reasons IE use the default content type still. So we add this option contentType to resolve the issue.
- dataType: The type of data that you're expecting back from the server. Review documentation and return to Dave's post for more details
- success: A function to be called if the request succeeds. The function gets passed two arguments: The data returned from the server, formatted according to the 'dataType' parameter, and a string describing the status. I used it to display the sliding DIV and set the returned data to the DIV HTML.
- error: A function to be called if the request fails. The function gets passed three arguments: The XMLHttpRequest object, a string describing the type of error that occurred and an optional exception object, if one occurred.
The following show the complete JavaScript call for the showhide method:
1: //master: id of div element that contains the information about master data
2: //details: id of div element wrapping the details grid
3: //customerId: id of the customer to be send as parameter to web method
4: function showhide(master,detail,customerId)
5: { 6: //First child of master div is the image
7: var src = $(master).children()[0].src;
8: //Switch image from (+) to (-) or vice versa.
9: if(src.endsWith("plus.png")) 10: src = src.replace('plus.png','minus.png'); 11: else
12: src = src.replace('minus.png','plus.png'); 13: //if the detail DIV is empty Initiate AJAX Call, if not that means DIV already populated with data
14: if($(detail).html() == "")
15: { 16: //Prepare Progress Image
17: var $offset = $(master).offset();
18: $('#progress').css('visibility','visible'); 19: $('#progress').css('top',$offset.top); 20: $('#progress').css('left',$offset.left+$(master).width()); 21: //Prepare Parameters
22: var params = '{customerId:"'+ customerId +'"}'; 23: //Issue AJAX Call
24: $.ajax({ 25: type: "POST", //POST
26: url: "GridViewDrillDownjQueryQAjax.aspx/GetOrders", //Set call to Page Method
27: data: params, // Set Method Params
28: beforeSend: function(xhr) { 29: xhr.setRequestHeader("Content-type", "application/json; charset=utf-8");}, 30: contentType: "application/json; charset=utf-8", //Set Content-Type
31: dataType: "json", // Set return Data Type
32: success: function(msg, status) { 33: $('#progress').css('visibility','hidden'); 34: $(master).children()[0].src = src;
35: $(detail).html(msg);
36: $(detail).slideToggle("normal"); // Succes Callback 37: },
38: error: function(xhr,msg,e){ 39: alert(msg);//Error Callback
40: }
41: });
42: }
43: else
44: { 45: //Toggle expand/collapse
46: $(detail).slideToggle("normal"); 47: $(master).children()[0].src = src;
48: }
49: }
NOTE:
There is something I need to mentione here, when I uploaded my sample to my hosting, I received an error while testing. When I viewed the error using FireBug, I noticed that the Content-Length is not send with the request header and it is mandatory. That never happen in my development environement. So I had to add the following line in the beforeSend function:
xhr.setRequestHeader("Content-length", params.length);
Conclusion:
I didn't need a real Web Service to actually retrieve the data. I thought that using Page Method is just enough and satisfy my needs. I think, not everything should be made as Web Service, my requirements do not specify that the Gustomer Orders should be exposed through a Web Service, specially that I wish to return a formatted HTML fragment in JSON form. So, Page Method was ideal for me.
Everytime I work with jQuery I reliaze how far it is powerful and easy to use. You can download the demo project (62.21 kb) to explore the whole code and view the demo to see how it is work.
I hope you find this post helpful with value.
