Wednesday, 28 December 2011

GridView Grouping Master/Detail Drill Down using AJAX and jQuery

GridViewDrillDownJQueryAjax00  GridViewDrillDownJQueryAjax01


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">&nbsp;</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:         });

  • url: URL of the distination I issue a request for (Page or Web Service) attached to is method name I want to invoke.
  • 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.
  • 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 Web Host, 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

No comments:

Post a Comment