Allen的自由天空

有财富未必有人生,有人生未必有财富

导航

ASP.NET 2.0's Client Callback Feature(转)

 

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.

callback event order

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.

client side explorer

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.


posted on 2008-04-01 19:48  AllenFeng  阅读(356)  评论(0编辑  收藏  举报