This post concentrates on how to show GridView on Hierarchical style with AJAX implementation. I have already blogged one post related to the same concept without AJAX implementation. This post takes the same scenario and implementing the same concept using XML HTTP AJAX implementation.
The use case of the implementation follows:
- The page should show a Grid View with list of Orders with Expand icon on the first column.
- On click of expand icon on each row, the Order Details of the Order must be fetched from the database and show in the Grid View just below to the row with little adjacent to the first column.
- The child Grid View must fetch from the server only on demand to avoid the loading time using AJAX concept.
- Once the expand icon clicked, there should be a loading message to inform to the user the details are fetching from the server.
- Once the Grid View fetched from the server, the Child Grid should show by hiding the loading message and the expand icon must be changed to collapse icon.
- On click of collapse icon, the child grid should hide from the screen.
- When the user clicks the Expand which was already trigged before (and Child Grid details fetched from the server) there should not be a call to the server again fetching the same details. The system should show the hidden Grid View.
- When the user clicks expand icon before previous expand process completed, the previous process (or loading) must be suspended and the new process should start immediately. (To test this requirement, I coded a sleep statement in the program.)
To know how XML HTTP AJAX works and how to implement the same in ASP.NET page, you can have a look on the post How to implement XML HTTP AJAX in ASP.NET Web Pages.
I am using Northwind database in this example. So to test the code, please make sure you have Northwind database and update the server in connection string on web.config.
I have two aspx page to implement this sample.
- Default.aspx – is the page end user accessing. This page contains the main grid which has the Order details. This page had XML HTTP AJAX implementation which calls another page ChildGridBuilder.aspx to get the child Grid View response.
- ChildGridBuilder.aspx – is the page act as remote page. This page generates the Child Grid View response which contains the list of Order Details information. As the Grid View is depends on an Order Id, it accepts a Query String for OrderId information. So the Default.aspx page will call this page with the Order Id which is from the expanding row.
The implementation follows:
Default.aspx GridView Script
<asp:GridView id="GridViewHierachical" runat="server" AutoGenerateColumns="False" Width="100%" DataKeyNames="OrderId" AllowPaging="true" CssClass="GridViewStyle" BorderStyle="Solid" PageSize="10" CellPadding="0" CellSpacing="1" OnPageIndexChanging="GridViewHierachical_PageIndexChanging" EnableViewState="false"> <Columns> <asp:TemplateField HeaderText="Order ID"> <ItemStyle Width="10%" /> <ItemTemplate> <input id='hid<%# Eval("OrderID") %>') value='0' type="hidden" /> <img id="img<%# Eval("OrderID") %>" alt="" width="9px" border="0" src="Images/plus.png" style="cursor:pointer;padding-left:3px;width:12px;height:12px;" onclick="javascript:GetChildGrid('<%# Eval("OrderID") %>');"/> <asp:Label ID="lblOrderID" Text='<%# Eval("OrderID") %>' Visible="true" runat="server"></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:BoundField DataField="CustomerID" HeaderText="Customer ID"> <ItemStyle Width="10%" /> </asp:BoundField> <asp:BoundField DataField="ShippedDate" HeaderText="Shipped Date" DataFormatString="{0:MMMM d, yyyy}"> <ItemStyle Width="20%" /> </asp:BoundField> <asp:BoundField DataField="ShipName" HeaderText="Ship Name"> <ItemStyle Width="20%" /> </asp:BoundField> <asp:BoundField DataField="ShipCountry" HeaderText="Ship Country"> <ItemStyle Width="15%" /> </asp:BoundField> <asp:BoundField DataField="ShipCity" HeaderText="Ship City"> <ItemStyle Width="15%" /> </asp:BoundField> <asp:TemplateField> <HeaderStyle CssClass="InvisibleCol" /> <ItemStyle CssClass="InvisibleCol" /> <ItemTemplate> </td></tr> <tr id="tr<%# Eval("OrderID") %>" style="display:none;"> <td colspan="6"> <div id="div<%# Eval("OrderID") %>" style="position:relative;left:10px;overflow:auto;width:99%;padding-bottom:1px;"></div> </ItemTemplate> </asp:TemplateField> </Columns> <FooterStyle BackColor="#CCCCCC" ForeColor="Black"></FooterStyle> <RowStyle CssClass="GridViewRow" Height="22px"></RowStyle> <HeaderStyle CssClass="GridViewHeader" Height="22px"></HeaderStyle> <SelectedRowStyle BackColor="#008A8C" Font-Bold="True" ForeColor="White"></SelectedRowStyle> <PagerStyle BackColor="#999999" ForeColor="Black" HorizontalAlign="Right"></PagerStyle> <AlternatingRowStyle CssClass="GridViewAlternativeRow"></AlternatingRowStyle> </asp:GridView>Default.aspx C# Code behind
protected void Page_Load(object sender, EventArgs e) { BindGrid(); } private void BindGrid() { using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLConnection"].ConnectionString)) { SqlCommand command = new SqlCommand("SELECT Orders.OrderID, Orders.CustomerID, Orders.ShippedDate, Orders.ShipName, Orders.ShipCountry, Orders.ShipCity FROM Orders", connection); command.CommandType = CommandType.Text; SqlDataAdapter dataAdapter = new SqlDataAdapter(command); connection.Open(); DataSet dsOrderDetails = new DataSet(); dataAdapter.Fill(dsOrderDetails); GridViewHierachical.DataSource = dsOrderDetails.Tables[0]; GridViewHierachical.DataBind(); } } protected void GridViewHierachical_PageIndexChanging(object sender, GridViewPageEventArgs e) { GridViewHierachical.PageIndex = e.NewPageIndex; BindGrid(); }Default.aspx Javascript Code
var is_ie = (navigator.userAgent.indexOf('MSIE') >= 0) ? 1 : 0; var is_ie5 = (navigator.appVersion.indexOf("MSIE 5.5") != -1) ? 1 : 0; var xmlHttp; /* This function requests the HTTPRequest, will be used to render the Dynamic content html markup * and it will call HandleResponse to handle the response */ function GetChildGrid(Id) { // Get the Div var childRow = document.getElementById("tr" + Id); var img = document.getElementById('img' + Id); if (childRow) { // Is already ChildGrid shown. If not shown if (childRow.style.display == "none") { // If the Child Grid is not fetched, then go and fetch it. if (document.getElementById('hid' + Id).value == '0') { var url = 'ChildGridBuilder.aspx?ID=' + Id; xmlHttp = createAjaxObject(); if (xmlHttp) { xmlHttp.open('get', url, true); xmlHttp.onreadystatechange = function() { HandleResponse(Id); } xmlHttp.send(null); } } else { childRow.style.display = "block"; img.src = "images/minus.png"; } } else { // Already Child Grid Shown childRow.style.display = "none"; img.src = "images/plus.png"; } } } /* This function is used to handler the http response */ function HandleResponse(Id) { var childGridDiv = document.getElementById("div" + Id); var childRow = document.getElementById("tr" + Id); var img = document.getElementById('img' + Id); // If Response completed if (xmlHttp.readyState == 4) { if (xmlHttp.status == 200) { // Here is the response var str = xmlHttp.responseText; childGridDiv.innerHTML = str; xmlHttp.abort(); // Mark the flag, Child Grid is fetched from server document.getElementById('hid' + Id).value = '1'; } else { childRow.style.display = "none"; img.src = "images/plus.png"; } } else { // Show the Progress status childGridDiv.innerHTML = "<div style='padding:4px;'><img src='Images/ajax-loader.gif' alt='' /><span style='color:blue;font-size:17px;font-weight:bold;'>Loading... Please wait</span></div>"; childRow.style.display = "block"; img.src = "images/minus.png"; } } /* function to create Ajax object */ function createAjaxObject() { var ro; var browser = navigator.appName; if (browser == "Microsoft Internet Explorer") { if (xmlHttp != null) { xmlHttp.abort(); } ro = new ActiveXObject("Microsoft.XMLHTTP"); } else { if (xmlHttp != null) { xmlHttp.abort(); } ro = new XMLHttpRequest(); } return ro; } /* Get the XML Http Object */ function GetXmlHttpObject(handler) { var objXmlHttp = null; if (is_ie) { var strObjName = (is_ie5) ? 'Microsoft.XMLHTTP' : 'Msxml2.XMLHTTP'; try { objXmlHttp = new ActiveXObject(strObjName); objXmlHttp.onreadystatechange = handler; } catch (e) { alert('Object could not be created'); return; } } return objXmlHttp; } function xmlHttp_Get(xmlhttp, url) { xmlhttp.open('GET', url, true); xmlhttp.send(null); }ChildGridBuilder.aspx GridView
<asp:GridView id="GridViewDetails" runat="server" AllowSorting="false" AutoGenerateColumns="False" Width="100%" CellSpacing="1" CellPadding="0" GridLines="Both" DataKeyNames="OrderId" BackColor="White" BorderWidth="2px" BorderStyle="Ridge" BorderColor="White" AllowPaging="false" ForeColor="#000066"> <Columns> <asp:BoundField DataField="OrderId" HeaderText="Order ID"> <ItemStyle Width="10%" /> </asp:BoundField> <asp:BoundField DataField="ProductName" HeaderText="Product Name"> <ItemStyle Width="30%" /> </asp:BoundField> <asp:BoundField DataField="UnitPrice" HeaderText="Unit Price" DataFormatString="{0:N}" ItemStyle-HorizontalAlign="Right"> <ItemStyle Width="15%" /> </asp:BoundField> <asp:BoundField DataField="Quantity" HeaderText="Quantity" DataFormatString="{0:N}" ItemStyle-HorizontalAlign="Right"> <ItemStyle Width="15%" /> </asp:BoundField> <asp:BoundField DataField="Discount" HeaderText="Discount" DataFormatString="{0:N}" ItemStyle-HorizontalAlign="Right"> <ItemStyle Width="15%" /> </asp:BoundField> <asp:BoundField DataField="Amount" HeaderText="Amount" DataFormatString="{0:N}" ItemStyle-HorizontalAlign="Right"> <ItemStyle Width="15%" /> </asp:BoundField> </Columns> <RowStyle ForeColor="#000066" Height="20px"></RowStyle> <SelectedRowStyle BackColor="#669999" Font-Bold="True" ForeColor="White"></SelectedRowStyle> <PagerStyle BackColor="White" ForeColor="#000066" HorizontalAlign="Left"></PagerStyle> <HeaderStyle CssClass="GridViewHeader"></HeaderStyle> <AlternatingRowStyle BorderStyle="Solid" BorderWidth="0px"></AlternatingRowStyle> </asp:GridView>ChildGridBuilder.aspx Code behind
protected void Page_Load(object sender, EventArgs e) { Response.Clear(); Response.ContentType = "text/xml"; BindGrid(); System.IO.StringWriter stringWrite = new System.IO.StringWriter(); System.Web.UI.HtmlTextWriter htmlWrite = new HtmlTextWriter(stringWrite); GridViewDetails.RenderControl(htmlWrite); Response.Write(stringWrite.ToString()); Response.End(); } public override void VerifyRenderingInServerForm(Control control) { } private void BindGrid() { // I am delaying the response to see the Loading... message System.Threading.Thread.Sleep(1000); using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLConnection"].ConnectionString)) { SqlCommand command = new SqlCommand("SELECT OrderDetails.OrderID, OrderDetails.ProductID, Products.ProductName," + "OrderDetails.UnitPrice, OrderDetails.Quantity, OrderDetails.Discount, " + "((OrderDetails.UnitPrice * OrderDetails.Quantity) - OrderDetails.Discount) Amount " + "FROM [Order Details] OrderDetails " + "JOIN Products ON Products.ProductID = OrderDetails.ProductID " + "WHERE OrderDetails.OrderID = '" + Request.QueryString["Id"].ToString() + "'" , connection); command.CommandType = CommandType.Text; SqlDataAdapter dataAdapter = new SqlDataAdapter(command); connection.Open(); DataSet dsOrderDetails = new DataSet(); dataAdapter.Fill(dsOrderDetails); GridViewDetails.DataSource = dsOrderDetails.Tables[0]; GridViewDetails.DataBind(); } }CSS StyleSheet
.GridViewStyle { font-family:Calibri; font-size:15px; } .GridViewHeader { background: url(../../Images/header.png) repeat-x 0px 0px; } .GridViewRow { } .GridViewAlternativeRow { background-color:#edf5ff; } .InvisibleCol { display:none; }
download the working example of the source code in C# here and in VB here
[Update 01 Nov 2011]
The above implementation shows, when clicking the expand icon in the row - it calls the server to get the GridView script and show in the page. Once the GridView shown, it never call again till the page refresh happening and it will just hide and show the GridView.
But some of our requirement is little different-
- Whenever the expand button calls, it should call the server to get the GridView script and show in the page. So, it shows the latest GridView data by going to the server every time when clicking the expand button.
download the example in C# here and in VB here.
- It will function like the same way in blog implementation, but additionally with Refresh button. So when expand button clicked, it will call the server to get the GridView script. It will show and hide as per expand button and wont call server once it shown in the page. The GridView also contains a Refresh button and when clicking the button it will call the server to refresh the data. So the data will be fetched from the server on the first time it expand and when the user clicks Refresh button.
download the example in C# here and in VB here.
[Update]
I have blogged another post on the same topic using AJAX and JQuery concepts. Below are the links
Hierarchical GridView in ASP.NET
Hierarchical GridView in ASP.NET with AJAX, JQuery implementation - Part 1
Hierarchical GridView in ASP.NET with AJAX, JQuery implementation - Part 2
Below is the output of the screen,
Initial Page |
Loading message when loading the Child Grid when expand button clicked |
Hierarchical Grid View with some child grid expanded |
Below is the Video output
Hi Thiru, Thanks for the wonderful blog. It really helped me. I have implemented the lazy loading exactly as described in the blog. However, there is one more thing I want to implement. Suppose, the OrderIds in the inner gridview are link buttons and I want to open a PDF when clicked on them. I have added OnRowCommand and its handler method to the inner grid view but the event never gets fired. I have tried placing the event handler method in both master and child .cs files but I still cannot make it work. Any ideas/inputs are greatly appreciated. Thanks for the informative blog again!
Hi Harish. The server events won’t be called from Child Grid as the grid rendered in JavaScript using AJAX and there is no proper post back for getting this Grid View. We have to achieve in some other way around.
There are two ways I know.
1. Directly give the url in the hyperlink (For Ex: http://domainname.com/document/10234.pdf). So when clicking the hyperlink, the response directly opens the pdf.
Download the source from http://www.box.net/shared/9qpu84bqhs6505ini7lg. In this example I have two pdf files linked with the child Grid. You can even get the url from database and bind it directly (only necessary is to be access the url using browser).
2. Call another method using an AJAX (can use GenericHandler in simpler way) and write code to return as response to popup as pdf file.
Verify this http://www.dotnettwitter.com/2011/09/uploading-and-downloading-files-any.html. In this, for downloading files I am using DownloadFile.ashx and calling from hyperlink in GridView (line 26). You can use any server code and return the pdf as response (what u use in OnRowCommand). If required, I can provide a working example to you.
Thank you for this! I wanted to have a nested gridview but due to the size of the grid, I knew it would heavily tax system resources with the number of calls to the database it would require. This "lazy loading" has definitely helped -- now it only calls back those rows which the user selects to see!
A fantastic walkthrough!
How can i reset the childgridview if i click a button so it loads new data to the chilgridview. Now it remembers every thing?
Please help and thank you for the turtorial!
It works in chrome but not in Explorer 9? what can i do??
Hi Hissi,
I have updated the post with another two example. Pls review the "[Update 01 Nov 2011]" section.
For me IE works fine, but Chrome, Firefox and Opera have some issue. It shows on the first column only. Let me know what is the issue you are getting.
My issue is for example when i load the root gridview and i want the data between 10/31-2011 and 11/01-2011 and click a button to load the grid, then click the arrow it shows my child grid and the data between the dates and that works fine, and it remembers what have been loaded in the child grid. But if i for example want to reload the root grid with new dates the root updates but the information in the child grid is still the same as the last time i loaded it.
I hope you get the question, and again thank you for your work.
I have tried your updates, and it is almost what i mean. The solution #1 where you call the child grid every time works, but is it possible to do so when you click a button (not in the gridview, and the button and gridview is in an updatepanel) it reloads the script.
My scenario:
[Calendar1] [Calendar2] [ButtonSearch]
[Gridview] [Child Gridview]
I choose date1 = 10/31-2011 date2 = 11/02-2011
Grdivew loads on ButtonSearch click, and when i click the arrow in the grid the child grid loads.
But when i choose new dates like date1 = 09/15-2011 date2 = 10/02-2011 and click the search button the root gridview loades but now the child grid remembers the old data and doesn't update with the new dates.
Hissi,
Here the Child Grid loads as per the data in the Parent Grid. For Ex: I have Order, OrderDetails - Order X contains ABC, Order Y contains DEF. So the Child Grid always depends on the parent Grid Row Id.
As per your requirement, if Child Grid values are not required to refresh, then no link between Parent Grid and Child Grid(because, when parent grid data changes it never affect the Child Grid).
Important to note here is, the Child Grid loads on AJAX call. So if the page refresh or Grid View refresh, the Child Grid will not shown. So your requirement can be done with more effort by storing some states and loading on refresh. It is not possible to use this example for this requirement.
Ok ill try, thank you for helping, and you update #1 helped alot ;-)
Got it to work, i just sended the parameters with chilgrid URL. Thank you for your help!
This comment has been removed by the author.
Hi,
The expanded rows collapse when the GridView is inside an Update panel with a timer tick
When any events fired on server, it will refresh the child grid and will be hidden. But when any event calls client side function wont refresh the child grid. You can send me some code to my mail, I can help you if required.
hi there,
Why does it show file is deleted or not allowed when i try to download any of the posted links.
Where can i get the files. please help!
Hi Derek Tan,
I could able to see the files in the mentioned urls. I am not sure why it shows like to you. Hope you may need to login to Box.com site. Please try again.
If you still face issue send a mail to me (pm.thirumalai@gmail.com) with list of files you need. I will reply back with files.
hi Thiru,
i have got your files via email. Many thanks and you are the best!
Hi Thiru,
Suppose for ChildGridBuilder.aspx, i want to add a column asp:ImageButton control with codebehind event, how should i go about it?
and upon clicking on the ImageButton will execute a javascript validation before executing the server event...
are you able to post up some code?
Thanks in advance!
Hi Derek Tan,
Having Javascript wont be an issue as it is going to be called before going to the server. For calling server events from the child grid, just verify the url and let me know if it is ok.
http://forums.asp.net/t/1747216.aspx/1?nested+gridview+with+Add+update+and+delete+
I will post another post with the requirement you mentioned in somedays.
I am looking forward to working through this code and hope it works for me. I've been searching all day for this very example to work and I will let you know how it works out. Thank you for the help!
Thiru, did you ever solve your issue with the child grid only showing the first column, because I am having that problem. Actually, the child grid shows all of its columns, but only within the 1st column of the parent grid, so it crams the entire child grid into the width of the 1st parent grid.
the screenshots above show it correctly, but I can't get it to work that way...any help would be appreciated.
Hi Pete Campbell,
I remember one time I seen it in browser other then IE. but not sure which browser it was. I will test the same code again with different browser and come back. It must be some css issue.
If you can let me know which browser and version you are using, it would be better. You can send screen shot to my mail id pm.thirumalai@gmail.com.
I figured out that it works only in IE 9 and below. I tested in IE 10, chrome, firefox, and safari and it all displays incorrectly in the 1st column like stated above.
I just sent screen shots from the different browsers to your email.
Thank you
Pete. Thanks for finding this issue...
I also got the child grid show in first cells with firefox browser (works fine with IE 9.0). I did some code change and verified with IE 9.0 and Firefox. I looks fine now.
I sent you source code by mail. Could you please verify with other browsers as well and let me know the output...
The same code available below url
https://www.box.com/s/afb230c2fce4525cc38d
Many Thanks..
The update fixed the problem and it works in IE 8, 9, 10, Chrome, Firefox, and Safari.
Thank you!!
Thanks Pete... :)
Where was the change made that fixes the first cell issue? Javascript only?
Hi Shawn,
The changes made both in GridView and Javascript. I had taken then "Loading... Please wait" message to GridView script and removed from Javascript.
That was the major change I have done.
You can look at the code once. The code located in below url.
https://www.box.com/s/afb230c2fce4525cc38d
Thanks for the code. But the loading message doesn't appear in Chrome. Only on Firefox and IE.
Hey Thiru
Thanks for the amazing post, In my child grid I added a button , but the gridview rowcammand and is not firing? I followed many articles but nothing seems to be working.
Also I have to set enablevalidation property to false on childbuilder as I was getting prerendor erros
Hi,
As this child grid rendered from another page using AJAX call and show in the parent grid as html script, the buttons added in the child grid will not work. But it the button calls a javascript function, then it will work.
You can verify the following post, which will handle exactly your requirement using AJAX call.
http://www.dotnettwitter.com/2012/06/hierarchical-gridview-in-aspnet-with_27.html
Let me know if you need any help.
Thanks again for the Reply , I have few more questions:
1) I tried to implement the solution provided by you i.e using jquery , Now in my scenario the hiddnen field that is being used , is in a user control,and now jquery is not able to recognize the this hddfield and i get error " hndExpandedChild doesnt exisits in current context" , how to solve this :(
2)If I use ajax implementation the approach I followed earlier, is there any was that I can access the values of ProductName, Discount etc, basically the datafields of child grid view?
Thanks a ton, Your help is much appericiated
Hi,
1) You can keep the Jquery script in the user control itself instead of in the page and to get the hidden control you need to use like
$('#<%= hndExpandedChild.ClientID %>').
For Ex: to get the value of hidden control use the following
val vHid = $('#<%= hndExpandedChild.ClientID %>').val();
and to se the value of the hidden control
$('#<%= hndExpandedChild.ClientID %>').val("1");
2) You can get the values from JQuery itself from Grid View instead of calling server. You just have to get the row and loop the cell and get the values.
Thanks Thiru, Great Help :)
What if I want to go one more level down after getting order details.
Hi friends,
how add a second level ?
Thanks !
Sir this is working in only IE. not working in other browser. can u help