Building Interactive Dynamic Pages Using Client Callbacks


If you have experience creating full-featured web applications using previous versions of ASP.NET, you have probably run across performance problems involving postbacks. The downside to using server-side events is that in order to trigger such an event, the page must completely reload itself, restore control and page state, and then render according to the new state.

Imagine that you were to create a page where you had three list boxes, and each time you click a list box, the subsequent list box populates with a list of child rows. This is a pretty common pattern, seen in applications where you select a country, then a state, then a city. It is also seen where you select increasing levels of detail from most to least general, such as selecting a company, then a location, then an office. Using purely server-side events, the entire page has to post back and potentially reload all of its data every time the user clicks a list box. This means that to get to the point where the user can proceed, he has clicked three times and caused two potentially slow postbacks.

One solution a lot of developers used in the past was to query all of the data on the first load and then render that data in the form of JavaScript client-side data structures. Then, each time the user clicked a list box, JavaScript would be used to populate the detail lists. This approach yields a pretty fast and reactive web page, but the initial load time becomes horrendous because the browser has to load and parse through mountains of JavaScript data initialization statements. The page size grows exponentially with the amount of data driving the page. This approach creates unwieldy server code and can often be slower than the purely server-side pattern.

The solution to this problem is an incredibly powerful mechanism that essentially allows JavaScript to invoke server-side methods on your page and then receive a response through a callback method that is also written in JavaScript. Callbacks and delegates were covered earlier in the book in Chapter 9, "Events and Delegates." This tool is referred to as a client callback, and ASP.NET 2.0 supports this functionality by default. Other tools created by third parties are also available that create similar functionality, such as Ajax .NET.

To allow a control to expose a method to client-side JavaScript, that control needs to implement the ICallbackEventHandler interface. This interface requires two methods:

  • void RaiseCallbackEvent(string eventArgument) This method is, through code you'll see shortly, invoked by JavaScript. The string argument is used to pass information to the method from JavaScript, such as the ID of the parent row for which to retrieve child rows.

  • string GetCallbackResult() This method is used to retrieve the results of the previous call to RaiseCallbackEvent. The results retrieval method is separate from the Callback Event method in order to allow JavaScript to make asynchronous calls in the background.

You can choose to implement this interface at the Page level, or at the Control level. If you choose to implement the interface at the Page level, you will find that you can only support one client callback for the entire page. If this is sufficient, then Page-level implementation is definitely the easiest way to go.

However, if you need to invoke multiple client callback methods (for example, you have more than one control that you want to dynamically populate without using an expensive server postback), you will need to create special controls to implement the ICallbackEventHandler interface.

Listing 22.4 contains the ASPx code that sets up a sample that will have three list boxes. The first list box is populated on the initial page load by the server. The second and third list boxes will be populated dynamically using client callbacks without incurring the overhead of a full server postback.

Listing 22.4. ASPx Containing Three List Boxes

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> <!DOCTYPE html PUBLIC   "-//W3C//DTD XHTML 1.0 Transitional//EN"   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Client Callback Demo</title> <script language="javascript"> function CallBackReturnFunction(sReturnValue, oContext) {     alert(sReturnValue);     var oLocations = document.forms[0].lbLocation;     var oOffices = document.forms[0].lbOffice;     oLocations.length = 0;     oOffices.length = 0;     if (sReturnValue == '') return;     var aLocations = sReturnValue.split('||');     for (var i=0;  i < aLocations.length; i++)     {         var aOptions = aLocations[i].split('||')         oLocations.options[oLocations.length] = new Option( aOptions[1], aOptions[0]);     } } function CallBackReturnFunction2(sReturnValue, oContext) {     var oOffices = document.forms[0].lbOffice;     oOffices.length = 0;     if (sReturnValue == '') return;     var aOffices = sReturnValue.split('||');     for (var i=0; i < aOffices.length; i++)     {         var aOptions = aOffices[i].split('|');         oOffices.options[oOffices.length] = new Option( aOptions[1], aOptions[0] );     } } function OnError(exception, context) {     alert(exception); } </script> </head> <body> <form  runat="server"> <div> <table width="500" border="0" cellspacing="2" cellpadding="2">     <tr>         <th>Company</th>         <th>Location</th>         <th>Office</th>     </tr>     <tr>         <td width=33% valign=top>             <asp:ListBox  runat="server"                 OnClick="OnCompanyClick(this.options[this.selectedIndex].value);"   />       </td>         <td width=33% valign=top>             <asp:ListBox  runat=server                 OnClick="OnLocationClick(this.options[this.selectedIndex].value);" />        </td>         <td width=33% valign=top>             <asp:ListBox  runat=server></asp:ListBox>         </td>     </tr>     <tr>         <td colspan=3 align=right>             <asp:Button  runat=server Text="View Office Details"/>         </td>     </tr> </table> </div> </form> </body> </html> 

The preceding ASP code should look pretty straightforward. There are three list boxes and a button. There are also two JavaScript functions: CallBackReturnFunction and CallBackReturnFunction2. These are both functions that will be called by the client callback framework when results have been made available by the server in response to their initial requests. These requests are made by additional JavaScript functions: OnCompanyClick and OnLocationClick. As you will see in Listing 22.5, these functions are generated dynamically using the RegisterClientScriptBlock method.

Listing 22.5. Page Code Corresponding to ASPx Code in Listing 22.4

[View full width]

using System; using System.Text; using System.Data.SqlClient; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class _Default : System.Web.UI.Page { public CompanyEventControl companyControl = new CompanyEventControl(); public LocationEventControl locationControl = new LocationEventControl(); protected void Page_Load(object sender, EventArgs e) {     ClientScriptManager cm = Page.ClientScript;     this.AddParsedSubObject(companyControl);     string cbRef = cm.GetCallbackEventReference(companyControl,         "companyId", "CallBackReturnFunction", "oContext", "OnError", false);     StringBuilder sb = new StringBuilder();     sb.Append(   "<script language=\"javascript\">function OnCompanyClick(companyId)\n");     sb.Append( "{ var oContext = new Object(); oContext.CommandName = \"GetLocationsByCompanyID\";\n");     sb.Append("oContext.CompanyID = companyId;\n");     sb.AppendFormat("{0};\n", cbRef);     sb.Append("}\n</script>");     cm.RegisterClientScriptBlock(this.GetType(), "OnCompanyClick", sb.ToString());     this.AddParsedSubObject(locationControl);     string cbRef2 = cm.GetCallbackEventReference(locationControl,         "locationId", "CallBackReturnFunction2", "oContext", "OnError", false);     sb = new StringBuilder();     sb.Append( "<script language=\"javascript\">function OnLocationClick(locationId)\n");     sb.Append( "{ var oContext = new Object(); oContext.CommandName = \"GetOfficesByLocation\";\n");     sb.Append("oContext.LocationID = locationId;\n");     sb.AppendFormat("{0}\n", cbRef2);     sb.Append("}\n</script>");     cm.RegisterClientScriptBlock(this.GetType(), "OnLocationClick", sb.ToString());     if (!Page.IsPostBack)         PopulateCompanies(); } void PopulateCompanies() {     SqlConnection conn = new SqlConnection("Data Source=localhost; Initial Catalog=SampleDB; Integrated  Security=SSPI;");     conn.Open();     SqlCommand cmd = conn.CreateCommand();     cmd.CommandText = "SELECT CompanyID, Description FROM Companies";     DataTable companies = new DataTable("Companies");     SqlDataAdapter da = new SqlDataAdapter(cmd);     da.Fill(companies);     lbCompany.DataTextField = "Description";     lbCompany.DataValueField = "CompanyID";     lbCompany.DataSource = companies;     lbCompany.DataBind(); } } 

Don't worry if this looks a little complex. As you progress through the book, the techniques used here will become second nature to you. Keep this sample handy as you go through the rest of the examples in the ASP.NET section and try to apply the technique of client callbacks to the other techniques you're learning.

The code in Listing 22.5 essentially indicates that two controls, CompanyEventControl and LocationEventControl, are going to be event handlers for client callbacks. Listing 22.6 contains the source code for both of these classes. Their sole purpose is to take a request indicating a parent row and return a specially serialized string (one that can be easily decomposed by JavaScript) containing the appropriate rows.

Listing 22.6. ICallbackEventHandler Implementations

using System; using System.Text; using System.Data; using System.Data.SqlClient; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; /// <summary> /// Summary description for CompanyEventControl /// </summary> public class CompanyEventControl : Control, ICallbackEventHandler { private DataTable locations; public CompanyEventControl() { // // TODO: Add constructor logic here // } #region ICallbackEventHandler Members public string GetCallbackResult() { StringBuilder sb = new StringBuilder(); foreach (DataRow row in locations.Rows) {     sb.AppendFormat("{0}|{1}||", row["LocationID"], row["Description"]); } return sb.ToString(); } public void RaiseCallbackEvent(string eventArgument) { using (SqlConnection conn = new SqlConnection(     "data source=localhost; initial catalog=SampleDB; Integrated Security=SSPI;")) {     conn.Open();     SqlCommand cmd = conn.CreateCommand();     cmd.CommandText = "SELECT LocationID, Description FROM Locations WHERE CompanyID = " +   eventArgument;     SqlDataAdapter da = new SqlDataAdapter(cmd);     locations = new DataTable();     da.Fill(locations); } } #endregion } public class LocationEventControl : Control, ICallbackEventHandler { private DataTable offices; public LocationEventControl() { // // TODO: Add constructor logic here // } #region ICallbackEventHandler Members public string GetCallbackResult() { StringBuilder sb = new StringBuilder(); foreach (DataRow row in offices.Rows) {     sb.AppendFormat("{0}|{1}||", row["OfficeID"], row["Description"]); } return sb.ToString(); } public void RaiseCallbackEvent(string eventArgument) { using (SqlConnection conn = new SqlConnection(     "data source=localhost; initial catalog=SampleDB; integrated security=SSPI;")) {     offices = new DataTable("Offices");     conn.Open();     SqlCommand cmd = conn.CreateCommand();     cmd.CommandText = "SELECT OfficeID, Description FROM Offices WHERE LocationID = " +   eventArgument;     SqlDataAdapter da = new SqlDataAdapter(cmd);     da.Fill(offices); } } #endregion } 

The code in Listing 22.6 responds to a client callback event triggered by client-side JavaScript and retrieves the appropriate child rows given the parent's ID (passed as the eventArgument parameter). Because JavaScript isn't going to be able to work with native .NET structures, the response to client callbacks is always going to be a string. In this case, the code used the pipe (|) and double-pipe (||) characters as column and row delimiters respectively.

Figure 22.4 shows a sample of what a page looks like that was populated using client callbacks. Keep in mind that the first list box is populated on the initial page load, and the second two list boxes are populated dynamically using client-side JavaScript and server-side client callback events.

Figure 22.4. Using client callbacks.


Using Client Callbacks

Even if you don't completely follow the code used to implement client callbacks, I strongly urge you to run the sample provided with the book. As you progress throughout the book, go back to this sample and, as a useful exercise, try to change the book's samples from purely server-side postbacks into samples that implement client callbacks for increased performance. The task of converting purely postback code to client callback code will rapidly increase your ASP.NET proficiency. Client callbacks are the steppingstone to Microsoft's Atlas implementation of AJAX technology, and understanding how they work will help ease the transition into AJAX-style application development.




Microsoft Visual C# 2005 Unleashed
Microsoft Visual C# 2005 Unleashed
ISBN: 0672327767
EAN: 2147483647
Year: 2004
Pages: 298

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net