Earlier this year Matt Berseth posted about Building a Grouping Grid with the ASP.NET 3.5 LinqDataSource and ListView Controls. Then I followed his post with 2 other posts discussing same idea but with different implementation:

Later I got few comments about how to implement that using AJAX, in order to display the details data on demand. At that time Dave was demonstrating the usage of jQuery AJAX on his post Using jQuery to consume ASP.NET JSON Web Services. So I came up with another demo to apply on demand loading of details data in my post GridView Grouping Master/Detail Drill Down using AJAX and jQuery.

Again I got few other comments on how to apply same approach, but using ASP.NET AJAX Control Toolkit CollapsiblePanelExtender control. So here I'm going to provide how to do that using CollapsiblePanelExtender and ASP.NET AJAX PageMethods call to apply on demand loading of details from the master data. You can download the sample and view Demo here.

A look inside CollapsiblePanelExtender Control

The reason why I didn't implement that earlier is that I though that CollapsiblePanelExtender do not expose client events when a panel is about to collapse or expand. But I turn out to be completely wrong. CollapsiblePanelExtender expose 6 useful client events that I can handle:

  1. Expanding
  2. Expanded
  3. ExpandComplete
  4. Collapsing
  5. Collapsed
  6. CollapseComplete

And you can register for these events using client script just as the following:

  1. function pageLoad(sender, args){  
  2.         var cpe = $find("CollapsiblePanelExtenderID");  
  3.         cpe.add_expandComplete(onExpand);  
  4.         cpe.add_collapseComplete(onCollapse);  
  5. }  
  6. function onExpand(s,e){  
  7.         //Code here  
  8. }  
  9. function onCollapse(s,e){  
  10.         //Code here  
function pageLoad(sender, args){
var cpe = $find("CollapsiblePanelExtenderID");
function onExpand(s,e){
//Code here
function onCollapse(s,e){
//Code here


I assume that you know that pageLoad function will be automatically registered for Application Load event.
I have no idea why these events is not exposed as Control Properties so registering would be much more easier using ASPX markup code like this:

  1. <ajaxcontroltoolkit:CollapsiblePanelExtender ID="cpe" runat="Server"   
  2.       ....  
  3.       OnExpand="onExpand"   
  4.       OnCollapse="onCollapse"/> 
<ajaxcontroltoolkit:CollapsiblePanelExtender ID="cpe" runat="Server" 


Extending the Extender

I like this way and I guess this would give much more cleaner code. So I just extended the CollapsiblePanelExtender control by inheriting for it and added 6 properties that map to the client events:

  1. public class CollapsiblePanelEx : CollapsiblePanelExtender  
  2. {  
  3.   [DefaultValue("")]  
  4.   [ExtenderControlEvent]  
  5.   [ClientPropertyName("expanding")]  
  6.   public string OnExpand  
  7.   {  
  8.      get { return (string)(ViewState["OnExpand"] ?? string.Empty); }  
  9.      set { ViewState["OnExpand"] = value; }  
  10.   }  
  11.   [DefaultValue("")]  
  12.   [ExtenderControlEvent]  
  13.   [ClientPropertyName("expanded")]  
  14.   public string OnExpanded  
  15.   {  
  16.       get { return (string)ViewState["OnExpanded"] ?? string.Empty; }  
  17.       set { ViewState["OnExpanded"] = value; }  
  18.   }  
  19.   [DefaultValue("")]  
  20.   [ExtenderControlEvent]  
  21.   [ClientPropertyName("expandComplete")]  
  22.   public string OnExpandComplete  
  23.   {  
  24.       get { return (string)ViewState["OnExpandComplete"] ?? string.Empty; }  
  25.       set { ViewState["OnExpandComplete"] = value; }  
  26.   }  
  27.   [DefaultValue("")]  
  28.   [ExtenderControlEvent]  
  29.   [ClientPropertyName("collapsing")]  
  30.   public string OnCollapse  
  31.   {  
  32.       get { return (string)ViewState["OnCollapse"] ?? string.Empty; }  
  33.       set { ViewState["OnCollapse"] = value; }  
  34.   }  
  35.   [DefaultValue("")]  
  36.   [ExtenderControlEvent]  
  37.   [ClientPropertyName("collapsed")]  
  38.   public string OnCollapsed  
  39.   {  
  40.       get { return (string)ViewState["OnCollapsed"] ?? string.Empty; }  
  41.       set { ViewState["OnCollapsed"] = value; }  
  42.   }  
  43.   [DefaultValue("")]  
  44.   [ExtenderControlEvent]  
  45.   [ClientPropertyName("collapseComplete")]  
  46.   public string OnCollapseComplete  
  47.   {  
  48.       get { return (string)ViewState["OnCollapseComplete"] ?? string.Empty; }  
  49.       set { ViewState["OnCollapseComplete"] = value; }  
  50.   }  
public class CollapsiblePanelEx : CollapsiblePanelExtender
public string OnExpand
get { return (string)(ViewState["OnExpand"] ?? string.Empty); }
set { ViewState["OnExpand"] = value; }
public string OnExpanded
get { return (string)ViewState["OnExpanded"] ?? string.Empty; }
set { ViewState["OnExpanded"] = value; }
public string OnExpandComplete
get { return (string)ViewState["OnExpandComplete"] ?? string.Empty; }
set { ViewState["OnExpandComplete"] = value; }
public string OnCollapse
get { return (string)ViewState["OnCollapse"] ?? string.Empty; }
set { ViewState["OnCollapse"] = value; }
public string OnCollapsed
get { return (string)ViewState["OnCollapsed"] ?? string.Empty; }
set { ViewState["OnCollapsed"] = value; }
public string OnCollapseComplete
get { return (string)ViewState["OnCollapseComplete"] ?? string.Empty; }
set { ViewState["OnCollapseComplete"] = value; }


Master/Details GridView

I'm using same UI and styles presented in my previous posts related to the same subject. But will explore the master grid when the grouping headers are specified as well as the collapsible panel that will hold the details:

  1. <asp:GridView ID="gvCustomers" Width="100%"   
  2.     AllowPaging="True" AutoGenerateColumns="False" 
  3.     DataSourceID="sqlDsCustomers"   
  4.     ShowHeader="False" runat="server" > 
  5.     <Columns> 
  6.         <asp:TemplateField> 
  7.             <ItemTemplate> 
  8.                 <asp:Panel Style="display:inline;"   
  9.                     CssClass="group" ID="pnlCustomer" runat="server"> 
  10.                     <asp:HiddenField ID="hdnCustId" runat="server"   
  11.                         Value='<%#Eval("CustomerID")%>' /> 
  12.                     <asp:Image ID="imgCollapsible"   
  13.                         ImageUrl="~/Assets/img/plus.png"   
  14.                         Style="margin-right: 5px;" 
  15.                         runat="server" /><span class="header"> 
  16.                             <%#Eval("CustomerID")%> 
  17.                             :  
  18.                             <%#Eval("CompanyName")%> 
  19.                             (<%#Eval("TotalOrders")%> 
  20.                             Orders) </span> 
  21.                 </asp:Panel> 
  22.                 <asp:Panel ID="pnlOrders" runat="server"   
  23.                     Style="margin-left: 20px; margin-right: 20px"  /> 
  24.                 <ajax:CollapsiblePanelEx ID="cpe" runat="Server"   
  25.                     TargetControlID="pnlOrders"   
  26.                     ExpandControlID="pnlCustomer"   
  27.                     CollapseControlID="pnlCustomer" 
  28.                     CollapsedSize="0" 
  29.                     Collapsed="True"   
  30.                     AutoCollapse="False"   
  31.                     AutoExpand="False"   
  32.                     ScrollContents="false"   
  33.                     ImageControlID="imgCollapsible" 
  34.                     ExpandedImage="~/Assets/img/minus.png"   
  35.                     CollapsedImage="~/Assets/img/plus.png" 
  36.                     ExpandDirection="Vertical"   
  37.                     OnExpand="onExpand" /> 
  38.             </ItemTemplate> 
  39.         </asp:TemplateField> 
  40.     </Columns> 
  41. </asp:GridView> 
<asp:GridView ID="gvCustomers" Width="100%" 
AllowPaging="True" AutoGenerateColumns="False"
ShowHeader="False" runat="server" >
<asp:Panel Style="display:inline;" 
CssClass="group" ID="pnlCustomer" runat="server">
<asp:HiddenField ID="hdnCustId" runat="server" 
Value='<%#Eval("CustomerID")%>' />
<asp:Image ID="imgCollapsible" 
Style="margin-right: 5px;"
runat="server" /><span class="header">
Orders) </span>
<asp:Panel ID="pnlOrders" runat="server" 
Style="margin-left: 20px; margin-right: 20px"  />
<ajax:CollapsiblePanelEx ID="cpe" runat="Server" 
OnExpand="onExpand" />

The target panel is "pnlOrders" and as you can see it is specified as TargetControlID of the CollapsiblePanelExtender. On line 37 I specified the client function that will handle OnExpand "expand" event of the CollapsiblePanelExtender. The function is called onExpand. And later I'll go through its code to see how it initiate AJAX request and receives the response.

On line 10 I declared a hidden field to store CustomerID so I can retrieve it later using client script. This CustomerID will be used as parameter while invoking PageMethod.

The Details

Just as how I made it when I built jQuery sample. The details data is placed in some user control using data Repeater control. That user control is loaded dynamically and executed in virtual page and return the page output as the client in the response. The code that perform this operation is placed with in static Page Method which we will explore later.

Below is ASPX markup of the user control that will display the details data.

  1. <script runat="server"> 
  2. protected override void OnLoad(EventArgs e)  
  3. {  
  4.   sqlDsOrders.SelectParameters["CustomerID"].DefaultValue = this.CustomerId;  
  5.   base.OnLoad(e);  
  6. }  
  7. </script> 
  9. <asp:SqlDataSource ID="sqlDsOrders" runat="server" 
  10.     ConnectionString="<%$ ConnectionStrings:Northwind %>" 
  11.         SelectCommand="SELECT [OrderID], [OrderDate],   
  12.         [RequiredDate], [Freight], [ShippedDate]  
  13.         FROM [Orders] WHERE ([CustomerID] = @CustomerID)"> 
  14.     <SelectParameters> 
  15.         <asp:Parameter Name="CustomerID" Type="String" DefaultValue="" /> 
  16.     </SelectParameters> 
  17. </asp:SqlDataSource> 
  18. <asp:Repeater ID="List" DataSourceID="sqlDsOrders" runat="server"> 
  19.     <HeaderTemplate> 
  20.         <table class="grid" cellspacing="0"   
  21.     rules="all" border="1"   
  22.     style="border-collapse: collapse;"> 
  23.             <tr> 
  24.                 <th scope="col">&nbsp;</th> 
  25.                 <th scope="col">Order ID</th> 
  26.                 <th scope="col">Date Ordered</th> 
  27.                 <th scope="col">Date Required</th> 
  28.                 <th scope="col" style="text-align: right;">Freight</th> 
  29.                 <th scope="col">Date Shipped</th> 
  30.             </tr> 
  31.     </HeaderTemplate> 
  32.     <ItemTemplate> 
  33.         <tr class='<%# (Container.ItemIndex%2==0) ? "row" : "altrow" %>'> 
  34.             <td class="rownum"> 
  35.                <%# Container.ItemIndex+1 %> 
  36.             </td> 
  37.             <td style="width: 80px;"> 
  38.                <%# Eval("OrderID") %> 
  39.             </td> 
  40.             <td style="width: 100px;"> 
  41.                <%# Eval("OrderDate","{0:dd/MM/yyyy}") %> 
  42.             </td> 
  43.             <td style="width: 110px;"> 
  44.                <%# Eval("RequiredDate", "{0:dd/MM/yyyy}")%> 
  45.             </td> 
  46.             <td style="width: 50px; text-align: right;"> 
  47.                <%# Eval("Freight","{0:F2}") %> 
  48.             </td> 
  49.             <td style="width: 100px;"> 
  50.                <%# Eval("ShippedDate", "{0:dd/MM/yyyy}")%> 
  51.             </td> 
  52.         </tr> 
  53.     </ItemTemplate> 
  54.     <FooterTemplate> 
  55.         </table> 
  56.     </FooterTemplate> 
  57. </asp:Repeater> 
<script runat="server">
protected override void OnLoad(EventArgs e)
sqlDsOrders.SelectParameters["CustomerID"].DefaultValue = this.CustomerId;
<asp:SqlDataSource ID="sqlDsOrders" runat="server"
ConnectionString="<%$ ConnectionStrings:Northwind %>"
SelectCommand="SELECT [OrderID], [OrderDate], 
[RequiredDate], [Freight], [ShippedDate]
FROM [Orders] WHERE ([CustomerID] = @CustomerID)">
<asp:Parameter Name="CustomerID" Type="String" DefaultValue="" />
<asp:Repeater ID="List" DataSourceID="sqlDsOrders" runat="server">
<table class="grid" cellspacing="0" 
rules="all" border="1" 
style="border-collapse: collapse;">
<th scope="col">&nbsp;</th>
<th scope="col">Order ID</th>
<th scope="col">Date Ordered</th>
<th scope="col">Date Required</th>
<th scope="col" style="text-align: right;">Freight</th>
<th scope="col">Date Shipped</th>
<tr class='<%# (Container.ItemIndex%2==0) ? "row" : "altrow" %>'>
<td class="rownum">
<%# Container.ItemIndex+1 %>
<td style="width: 80px;">
<%# Eval("OrderID") %>
<td style="width: 100px;">
<%# Eval("OrderDate","{0:dd/MM/yyyy}") %>
<td style="width: 110px;">
<%# Eval("RequiredDate", "{0:dd/MM/yyyy}")%>
<td style="width: 50px; text-align: right;">
<%# Eval("Freight","{0:F2}") %>
<td style="width: 100px;">
<%# Eval("ShippedDate", "{0:dd/MM/yyyy}")%>


Static PageMethod

The method is simple. It just create new Page instance, load the user control in that Page then perform Server.Execute to get serviced page output just as the following:

  1. [System.Web.Services.WebMethod()]  
  2. public static string GetOrders(string customerId)  
  3. {  
  4.   Page page = new Page();  
  5.   CustomerOrders ctl = (CustomerOrders)page.LoadControl("~/CustomerOrders.ascx");  
  6.   ctl.CustomerId = customerId;  
  7.   page.Controls.Add(ctl);  
  8.   System.IO.StringWriter writer = new System.IO.StringWriter();  
  9.   HttpContext.Current.Server.Execute(page, writer, false);  
  10.   string output = writer.ToString();  
  11.   writer.Close();  
  12.   return output;  
public static string GetOrders(string customerId)
Page page = new Page();
CustomerOrders ctl = (CustomerOrders)page.LoadControl("~/CustomerOrders.ascx");
ctl.CustomerId = customerId;
System.IO.StringWriter writer = new System.IO.StringWriter();
HttpContext.Current.Server.Execute(page, writer, false);
string output = writer.ToString();
return output;


The method accepts one parameter customerId. It sets this parameter to the UserControl's CustomerId Property. The AJAX Request that is will invoke this method will send this parameter through the request.

Calling PageMethods Using ASP.NET AJAX

I'm sure you might passed by such subject before. Nothing new, we will just issue an AJAX call from the CollapsiblePanelExtender expand event handler function "onExpand" that specified earlier in the declaration of the CollapsiblePanelExtender.

First of all I need to enable PageMethods on the ScriptManager. Actually I'm using the AjaxTookKit one:

  1. <ajaxToolkit:ToolkitScriptManager   
  2.     ID="ScriptManager1"   
  3.     EnablePageMethods="true"   
  4.     runat="server" /> 
runat="server" />


Time to explore the expand event handler "onExpand". The handler initiate AJAX Call by invoking PageMethod.GetOrders. Don't worry you don't have to worry from where this come from, it is automatically generated once you enabled your ScriptManager for PageMethods calls.

The client method PageMethod.GetOrders accepts 4 parameters.

  • First parameter is the customerId.
  • second parameter is a callback function when the PageMethod is successfully invoked.
  • Third parameter is also a callback function that is called when an error occur, that means AJAX call is failed.
  • Forth and last parameter is known as user context, it like a data parameter you pass to this method to be able to receive it again in any of the callback function mentioned earlier.


  1. PageMethods.GetOrders(custId, OnSucceeded, OnFailed, sender); 
PageMethods.GetOrders(custId, OnSucceeded, OnFailed, sender);
Below is the code for the CollapsiblePanelExtender expand event handler:
  1. //Sender: Reference to the CollapsiblePanelExtender Client Behavior  
  2. //eventArgs: Empty EvenArgs  
  3. function onExpand(sender, eventArgs)  
  4. {  
  5.   //Use sender (instance of CollapsiblePanerExtender client Behavior)  
  6.   //to get ExpandControlID.  
  7.   var expander = $get(sender.get_ExpandControlID());  
  9.   //Get bounds of the expand control,   
  10.   //will be used to set position of progess indicator.  
  11.   var bounds = Sys.UI.DomElement.getBounds(expander);  
  13.   //Progress Indicator element  
  14.   var progress = $get('progress');  
  16.   //Set Position & visibility of Pogress Indicator  
  17.   Sys.UI.DomElement.setLocation(progress,bounds.x+bounds.width,bounds.y);  
  18.   progress.style.visibility = 'visible';  
  20.   //Using RegEx to replace pnlCustomer with hdnCustId.  
  21.   //hdnCustId is a hidden field located within pnlCustomer.  
  22.   //pnlCustomer is a Panel, and Panels are not Naming Container.  
  23.   //So hdnCustId will have the same ID as pnlCustomer but with   
  24.   //'hdnCustId' at the end insted of pnlCustomer.  
  25.   var custId = $get(sender.get_ExpandControlID().replace(/pnlCustomer/g,'hdnCustId')).value;  
  27.   //Issue AJAX call to PageMethod, and send sender   
  28.   //object as userContext Parameter.  
  29.   PageMethods.GetOrders(custId, OnSucceeded, OnFailed, sender);  
//Sender: Reference to the CollapsiblePanelExtender Client Behavior
//eventArgs: Empty EvenArgs
function onExpand(sender, eventArgs)
//Use sender (instance of CollapsiblePanerExtender client Behavior)
//to get ExpandControlID.
var expander = $get(sender.get_ExpandControlID());
//Get bounds of the expand control, 
//will be used to set position of progess indicator.
var bounds = Sys.UI.DomElement.getBounds(expander);
//Progress Indicator element
var progress = $get('progress');
//Set Position & visibility of Pogress Indicator
progress.style.visibility = 'visible';
//Using RegEx to replace pnlCustomer with hdnCustId.
//hdnCustId is a hidden field located within pnlCustomer.
//pnlCustomer is a Panel, and Panels are not Naming Container.
//So hdnCustId will have the same ID as pnlCustomer but with 
//'hdnCustId' at the end insted of pnlCustomer.
var custId = $get(sender.get_ExpandControlID().replace(/pnlCustomer/g,'hdnCustId')).value;
//Issue AJAX call to PageMethod, and send sender 
//object as userContext Parameter.
PageMethods.GetOrders(custId, OnSucceeded, OnFailed, sender);


sender Parameter is a reference to the CollapsiblePanelExtender. So it is easy to reach to the target control to be expanded as well as expand control "expander". At like 29 I'm sending the sender "CollapsiblePanelExtender Reference" as the user context; So I'll be able to continue processing when OnSucceeded callback function is invoked if the call is successfully made.

So time to explore the callback functions:

  1. // Callback function invoked on successful   
  2. // completion of the page method.  
  3. function OnSucceeded(result, userContext, methodName)   
  4. {  
  5.   $get('progress').style.visibility = 'hidden';  
  6.   //userContext is sent while issue AJAX call  
  7.   //it is an instance of CollapsiblePanelExtender client Behavior.  
  8.   //Used to get the collapsible element and sent its innerHTML   
  9.   //to the returned result.              
  10.   userContext.get_element().innerHTML = result;  
  11. }  
  12. // Callback function invoked on failure   
  13. // of the page method.  
  14. function OnFailed(error, userContext, methodName)   
  15. {  
  16.   $get('progress').style.visibility = 'hidden';  
  17.   alert(error.get_message());  
// Callback function invoked on successful 
// completion of the page method.
function OnSucceeded(result, userContext, methodName) 
$get('progress').style.visibility = 'hidden';
//userContext is sent while issue AJAX call
//it is an instance of CollapsiblePanelExtender client Behavior.
//Used to get the collapsible element and sent its innerHTML 
//to the returned result.            
userContext.get_element().innerHTML = result;
// Callback function invoked on failure 
// of the page method.
function OnFailed(error, userContext, methodName) 
$get('progress').style.visibility = 'hidden';
At line 10 on the code snippet above is where exactly the details data being shown. result parameter contains the returned value of the PageMethod GetOrders. And the output is just fine HTML produced by the UserControl CustomerOrders.ascx.



The above is just another example of same approached presented in some of my earlier posts. Here I was demonstrating ASP.NET AjaxControlToolkit CollapsiblePanelExtender Control predefined events to apply on demand loading just as the way I presented in my previous post GridView Grouping Master/Detail Drill Down using AJAX and jQuery. I did not discuss performance as it is out of the context of this article.

I hope you enjoyed it, feel free to download the sample or view the demo.