One of the most overlooked features of ASP.NET 2.0, part of Visual Studio 2005
or "Whidbey", is the Client Callback feature. This feature allows
you to programmatically call server-side methods through client-side JavaScript
code without the need for posting back the page. This article describes how
to use the Client Callback feature to implement your own callback scenario and
introduces the new TreeView control that has this feature built in. The code
samples in this article were written using the "Whidbey" version
distributed at the Professional Developer's Conference in late 2003.
The Need for Client Callbacks
Due to the nature of the stateless HTTP protocol, every time the Web user requires
retrieving of data from the server or wants to invoke some code that needs to
be executed on the server-side, he or she has to submit the page first. On post-back,
event handlers will execute the code and serve the data back to the user whose
browser has to re-render the page. This event model works fine for most of the
cases, but imposes a few restrictions that ASP.NET developers have learned to
live with.
To keep the state of form input controls on the client, the developer either
has to work with extensive ViewState information that slows down the page retrieval
or has to write some complex programming logic. Secondly, re-rending the page
requires some processing time by the Web browser. For small pages this is not
a problem, but for a page with heavy DHTML use or simply lots of content, some
flickering can occur even if you use SmartNavigation and dial-up users will
notice the blue processing bar that appears at the bottom of the browser.
For these reasons, the ability to call server-side methods from client-side
code is a request that many Web developers have had for a long time. The good
news is that ASP.NET 2.0 has this feature built in.
How Client Callback Can Be Implemented Now
Before we go to ASP.NET 2.0’s Client Callback feature, let’s take a look at
what Web developers currently do to overcome this problem. Calling server-side
methods from JavaScript is something that is currently possible using Microsoft’s
XMLHTTP ActiveX object.
This ActiveX object allows you to retrieve XML files over the Internet using
the HTTP protocol. However, unlike the name implies, you can use this object
to issue an HTTP request to any server — including Classic ASP, regular HTML,
or even PHP files — and just retrieve the raw HTML output. Since the XMLHTTP
object is pretty much a standard ActiveX object, you can instantiate it using
regular JavaScript. So, let’s take a look at this sample code that retrieves
the HTML code from Google’s front page:
function RetrieveGoogleFrontPage() {
var XmlHttp = new ActiveXObject("Msxml2.XMLHTTP.4.0");
XmlHttp.Open("GET", "http://www.google.com", false);
XmlHttp.Send();
return XmlHttp.responseText;
}
|
From this code sample, you can see that using the XMLHTTP object is fairly
simple. You simply specify a URL to issue the request and retrieve the complete
content that is being returned from the Web server. All this is done in JavaScript,
so the page on which this code resides is actually not being posted back. Also,
notice that the XMLHTTP object returns the complete response text. This means
that if you just want to retrieve business data from the server side, you have
to write a special page that returns the business data with the unnecessary
HTML code that bloats a regular page.
ASP.NET 2.0's Client Callback
Now, let’s fast forward to ASP.NET 2.0. The new ASP.NET abstracts the use of
the XMLHTTP object. Internally the Client Callback feature still uses the XMLHTTP
object, but both the Web user as well as the Web developer are shielded from
it.
The Client Callback feature really consists of two things: the new ICallbackEventHandler
interface as well as the new Page.GetCallbackEventReference
method. The architecture boils down to the following basic steps.
The Page.GetCallbackEventReference method and its
overloads will create JavaScript code snippets that you need to place on the
client side. These code snippets contain code that sends an HTTP request back
to the page (using the XMLHTTP object under the hood). The request is then handled
on the server side by a Web control that implements the ICallbackEventHandler
interface. In most cases, that Web control is the page itself, but you can have
specific user controls or Web controls that react to the request, as you will
see later in this article. Once the request has been handled, the result is
then passed back to the client through another JavaScript function whose sole
purpose is to react to the result of the request.
Let’s take a look at this ASP.NET 2.0 code sample that simply retrieves the server time and displays it through a regular JavaScript alert:
<%@ page language="C#" compilewith="ServerTime.aspx.cs" classname="ASP.ServerTime_aspx" %>
<html>
<head>
<title>Server Time</title>
<script language="javascript">
function GetServerTime()
{
var message = '';
var context = '';
<%=sCallBackFunctionInvocation%>
}
function ShowServerTime(timeMessage, context) {
alert('The time on the server is:\n' + timeMessage);
}
function OnError(message, context) {
alert('An unhandled exception has occurred:\n' + message);
}
</script>
</head>
<body>
<form id="MainForm" runat="server">
<input type="button" value="Get Server Time" onclick="GetServerTime();" />
</form>
</body>
</html>
|
using System;
using System.Web.UI;
namespace ASP {
public partial class ServerTime_aspx : ICallbackEventHandler
{
public string sCallBackFunctionInvocation;
void Page_Load(object sender,System.EventArgs e)
{
sCallBackFunctionInvocation = this.GetCallbackEventReference(this,"message","ShowServerTime","context","OnError");
}
public string RaiseCallbackEvent(string eventArgument)
{
// Uncomment next line to test error handler
// throw new ApplicationException("Some unhandled exception");
return DateTime.Now.ToString();
}
}
}
|
The first thing you notice is that the Page implements
the ICallbackEventHandler interface. This interface
really has only one method, namely RaiseCallbackEvent.
This is the method that is being executed when a request is handled, and as
you can see in this example, it simply returns the current time on the server.
To create the client code, we are making a call to the Page.GetCallbackEventReference
method on page load. As stated before, this method will create the JavaScript
code snippet that, when invoked, will initiate the client callback. The GetCallbackEventReference
method has several overloads, but in all overloads one has to basically indicate
which Web control will react to the request (in this case, it’s our own page
instance), the JavaScript variable names that contain the parameters specific
to this request, and JavaScript functions that are being called when the request
returns or errors out.
Since this method returns the JavaScript code to initiate the callback, we simply wrap that string inside a JavaScript function that is invoked on the button click. At run time, the <%=sCallBackFunctionInvocation%> expression will evaluate to:
|
__doCallback('__Page',message,ShowServerTime,context,OnError)
|
__doCallback is an ASP.NET 2.0 internal JavaScript
function that will initiate the HTTP request to call back the server. Notice
how the variable and function names we specified in the GetCallbackEventReference
method directly translate into this function call. The pseudo-code in FIGURE
1 illustrates the event order that will take place when a callback is initiated.

FIGURE 1: Callback event order
Whe the callback initiates, the message variable is the main parameter that
will be passed to the server-side RaiseCallbackEvent
method. It is important to understand that a JavaScript string value is being
passed to a .NET method. Notice how this message has to be of type string, so
if you want to pass complex data back to the server, you have to apply some
serialization to your data structure to flatten it out as a string. In this
example, we do not need to pass any information to the server, so we just initialize
the message variable as an empty string. The same applies to the return value
of this method. Again, notice that a .NET string is being returned to a JavaScript
method as a string value.
The two JavaScript functions ShowServerTime and OnError
are self-explanatory. These are the functions that will be called from the server
upon finishing the client callback request. ShowServerTime’s
first parameter will hold the value of whatever is returned from the RaiseCallbackEvent
method (which in our case is just the server time). OnError’s
first parameter will hold the value of the Message
property of whatever unhandled exception has occurred on the server side.
The context variable is an interesting variable. Although we pass this variable
to the function call, it is not passed to the server-side method (as we know
that the RaiseCallbackEvent method has only one parameter).
Instead, the context variable is cached on the browser throughout the entire
callback and then passed as the second parameter to the returning JavaScript
functions. This will allow us to identify the context of this entire callback.
Imagine a scenario, where you are initiating several requests to the server
that really serve two separate events or a case where you have several concurrent
callbacks for the same event. You cannot be guaranteed that the returning JavaScript
methods are called in the same order in which the requests were initiated in,
so the context variable allows you to mark each request with a unique value
and act upon this value as it is being returned to the JavaScript functions.
If you run this example, you will see when clicking on the Show Server Time
button, that the entire event is handled in the background without the page
being refreshed. Also notice that the client callback is handled asynchronously,
so even if the server-side method might take several seconds or minutes to complete,
the client browser's user interface is not being blocked. Of course, if the
Web user navigates to a new page, the JavaScript functions won’t be invoked
anymore, but the RaiseCallbackEvent will finish to
its completion.
The New TreeView Control
At the beginning of the article, I mentioned that any Web control can implement
the ICallbackEventHandler interface. In our previous
example, I have reused the same page to implement this interface. However, you
can have your own user controls or even server controls implement this interface
as well. In fact, the new TreeView control shipped with ASP.NET 2.0 implements
this interface.
As the name implies, this control renders a tree view to display hierarchical
data on the client side. It’s a very rich control that can databind to a static
XML file on the server. You can also create the tree on-the-fly using the same
similar syntax as we know it from Windows Forms world. Now what’s cool about
this control is that when you set the PopulateNodesFromClient
and EnableClientScript property to True, you can allow
the TreeView to populate its children nodes through server-side data without
the need for posting back the page. Needless to say, this is achieved using
the ICallbackEventHandler, but even the use of this
interface has been completely abstracted for the developer.
Putting It All Together: The Client-Side Explorer
Let’s put this all together in an example where I use the TreeView control
to create a Windows Explorer-like user interface for the Web. This example allows
a Web client to navigate through the entire hard drive of the Web server and
obtain the properties of any file — all this without the need to post back the
page or pre-cache the entire data on the client side. FIGURE 2 shows what are
we are going to build.

FIGURE 2: Client Side Explorer
The page consists of an address bar TextBox at the top, the folder TreeView
on the left, and a regular file-listing ListBox on the right. Expanding the
tree nodes will populate the children nodes on demand. Selecting a tree node
will fill the list of files on the right and clicking on any file on the right
will display its file properties. Here's the code:
<%@ page language="C#" compilewith="default.aspx.cs" classname="ASP.default_aspx" enableviewstate="true" enablesessionstate="False" %>
<html>
<head runat="server">
<title>Client Side Explorer</title>
<script language="javascript">
function OnFolderClick(sFolderPath) {
var sMessage = sFolderPath;
// create context variable
var oContext = new Object();
oContext.CommandName = "GetFileListing";
oContext.FolderName = sFolderPath;
<%=sCallBackFunctionInvocation%>
}
function OnFileClick(sFileName) {
var oCurrentFolderBox = document.forms[0].CurrentFolder;
var sMessage = oCurrentFolderBox.value + "\\" + sFileName;
// create context variable
var oContext = new Object();
oContext.CommandName = "GetFileInformation";
oContext.FileName = sFileName;
<%=sCallBackFunctionInvocation%>
}
function CallBackReturnFunction(sReturnValue, oContext) {
if (oContext.CommandName == 'GetFileListing') {
// process results for folder listing
var oFileBox = document.forms[0].FileListing;
var oCurrentFolderBox = document.forms[0].CurrentFolder;
// set current folder path
oCurrentFolderBox.value = oContext.FolderName;
// deserialize file string
var aFiles = sReturnValue.split('|');
// clear current file listing
while(oFileBox.length > 0)
oFileBox.options[0] = null;
// create new file listing
for(i = 0; i<aFiles.length;i++)
oFileBox.options[oFileBox.length] = new Option(aFiles[i]);
} else if (oContext.CommandName == 'GetFileInformation') {
// process result for file information
alert('File Information for "' + oContext.FileName + '"\n\n'+ sReturnValue);
} else
alert('Invalid context.');
}
function OnCallBackError(exception,context) {
alert('Unhandled exception occurred:\n' + exception);
}
</script>
</head>
<body>
<form runat="server">
<h3>Client Side Explorer</h3>
<table style="align=center;background-color:#CCCCCC;width:90%;height:90%;border-style:solid;">
<tr style="height:20;">
<td colspan="2">
<input type="text" id="CurrentFolder"
style="width:100%" name="CurrentFolder"/>
</td>
</tr>
<tr>
<td style="width:200;background-color:white;vertical-align:top">
<asp:panel id="TreePanel"
runat="Server"
scrollbars="Auto"
style="width:100%;height:100%;"
borderstyle="inset"
>
<asp:treeview id="FolderTree" runat="server"
font-names="Tahoma"
font-size="8pt"
ImageSet="XP_Explorer"
NodeIndent="15"
ShowLines="true"
PathSeparator="\"
PopulateNodesFromClient="true"
EnableClientScript="true" />
</asp:panel>
</td>
<td>
<select id="FileListing"
size="2"
style="width:100%;height:100%"
OnClick="OnFileClick(this.options[this.selectedIndex].text)"/>
</td>
</tr>
</table>
</form>
</body>
</html>
|
Looking at the ASPX code above, you will see that I have placed the TreeView
inside a single Panel control for the sole purpose of making use of the panel’s
new scrollbars property. I declaratively use some of
the TreeView’s properties to control the appearance of the TreeView to resemble
that of the usual Windows Explorer. Notice that we are setting the PopulateNodesFromClient
property to True to indicate that the TreeView should issue a client callback
to the server each time a node is expanded, and since this requires some client-side
code, we also need to enable the EnableClientScript
property to True.
On the server side, we need write the code to handle the population of tree
nodes. On the initial load of the page, I am adding the root node that contains
the c:\ root directory as its value. Since I want all TreeNodes to populate
on demand, I need to set the PopulateOnDemand Property
to True. In addition, I am setting the NavigateUrl
property to call the OnFolderClick JavaScript function:
using System;
using System.Web.UI;
using System.IO;
using System.Text;
using System.Web.UI.WebControls;
namespace ASP {
public partial class default_aspx : ICallbackEventHandler
{
public string sCallBackFunctionInvocation;
void Page_Load ( object sender, System.EventArgs e )
{
// create callback code
sCallBackFunctionInvocation = this.GetCallbackEventReference(this, "sMessage", "CallBackReturnFunction", "oContext", "OnCallBackError") + ";";
// hook into tree node population event
FolderTree.TreeNodePopulate += new TreeNodeEventHandler(FolderTree_TreeNodePopulate);
// For the intial load, we need to add the root-node
if (!Page.IsPostBack)
{
TreeNode rootNode = new TreeNode(@"c:\","c:");
rootNode.NavigateUrl = @"javascript:OnFolderClick('c:\\');";
rootNode.PopulateOnDemand = true;
FolderTree.Nodes.Add(rootNode);
}
}
private void FolderTree_TreeNodePopulate ( object sender, TreeNodeEventArgs e )
{
// obtain the current Directory
DirectoryInfo currentDirectory = new DirectoryInfo(e.Node.ValuePath + FolderTree.PathSeparator);
if (currentDirectory.Exists)
{
// go through each sub directory
foreach (DirectoryInfo subDirectory in currentDirectory.GetDirectories())
{
// and create a new tree node for each of them
TreeNode subNode = new TreeNode(subDirectory.Name, subDirectory.Name);
subNode.NavigateUrl = "javascript:OnFolderClick('" + subDirectory.FullName.Replace("\\", "\\\\") + "');";
subNode.PopulateOnDemand = true;
// add new sub-node to the current node
e.Node.ChildNodes.Add(subNode);
}
}
}
public string RaiseCallbackEvent(string eventArgument)
{
StringBuilder sReturnValue = new StringBuilder();
// if a directory is requested
if (Directory.Exists(eventArgument))
{
// create a pipe-delimited list of sub directories
foreach (string sFile in Directory.GetFiles(eventArgument))
sReturnValue.Append(Path.GetFileName(sFile) + "|");
return sReturnValue.ToString().TrimEnd('|');
}
else if (File.Exists(eventArgument))
{
// if a file is requested
FileInfo oFile = new FileInfo(eventArgument);
// create message for file properties
sReturnValue.Append("File Size: \t" + oFile.Length / 1024 + " kb" + Environment.NewLine);
sReturnValue.Append("Creation Time:\t" + oFile.CreationTime + Environment.NewLine);
sReturnValue.Append("Access Time: \t" + oFile.LastAccessTime + Environment.NewLine);
sReturnValue.Append("Write Time: \t" + oFile.LastWriteTime + Environment.NewLine);
// return return value
return sReturnValue.ToString();
}
else
throw new System.ApplicationException("Invalid message (" + eventArgument + ") was passed to server.");
}
}
}
|
I also need to create an event handler for the TreeNodePopulate
event of the tree view. In the event handler, I simply obtain the full path
of the TreeNode (the Node.ValuePath property of a TreeNode
will concatenate all values of each TreeNode along its path into a single string),
and then use the DirectoryInfo.GetDirectories method
to iterate over all the subdirectories of the TreeNode that is being expanded.
For each subdirectory, I am creating a new TreeNode and adding it to the TreeView.
Please note that this method is being called from the client side when a tree
node is being expanded and that all the internal plumbing using Page.GetCallbackEventReference
and RaiseCallbackEvent is already abstracted for us.
What’s now left to do is to create the handlers to populate the ListBox with
files when a directory is selected and obtain file properties when a single
file is clicked.
These are two separate events, so I have to use the Client Callback architecture
as described in this article in a more tricky way. First, I use the Page.GetCallbackEventReference
method to generate the JavaScript that will initiate the client callback, but
since the RaiseCallbackEvent method will handle events
for both type of requests, I take a look at the string argument being passed
and return the file properties if the argument resembles a filename or return
a pipe-delimited list of filenames if the argument resembles a directory. This
analysis of the argument suffices for this example, but as mentioned before,
in more complex scenarios where you want to pass several arguments, you have
to combine them into as single string using delimited lists, XML, or your own
serialization technique.
On the client side, the OnFolderClick JavaScript method
will be called every time the user clicks on a node in the folder tree. This
has been achieved, by adding this JavaScript function call to every TreeNode
that I have created.
On the other hand, the OnFileClick JavaScript method
will be called when a filename is selected from the ListBox control. Since only
the filename is being passed to this JavaScript function, I obtain the full
path of the current directory from the address bar above.
Both JavaScript functions initiate the callback through the code that was generated
by the GetCallbackEventReference, so both functions
wrap around the call to the <%=sCallBackFunctionInvocation%>
expression. Now we have a case where it is necessary to use the context variable
to differentiate between these two types of events.
Therefore, in both function calls I instantiate a generic JavaScript object
with a CommandName property that I set to a hard-coded
string and a FolderName or FileName
property to hold the appropriate values. Notice how I am passing that entire
generic object as the context variable, so on the CallBackReturnFunction
method (my JavaScript event handler for the callback return), I will receive
this object with its existing properties. I now only have to inspect the CommandName
property to find out what type of event this is and react to it accordingly.
For a folder listing, I first update the address bar TextBox above and deserialize the pipe-delimited list of filenames and fill those into the ListBox. In the event of a filename selection, I simply display the file properties through a regular JavaScript alert box.
Conclusion
This article has shown several uses of ASP.NET 2.0's Client Callback feature.
We have seen how a simple page can call a server-side method and how several
concurrent callbacks should be managed. Also, the new TreeView control that
makes use of this feature internally was illustrated. Please note that not every
browser supports client callbacks, so two new boolean properties named SupportsCallback
and SupportsXmlHttp were added to the Request.Browser
object. Currently, both properties will return the same value, but the two properties
were created because client callbacks might be implemented using a different
technique other than the XMLHTTP ActiveX object in the future.
I hope I have helped you realize the great potential that the Client Callback
feature can bring to the Web development table. This feature is a very powerful
addition to ASP.NET and opens the door for richer user interfaces.