Cutting Edge

Custom Script Callbacks in ASP.NET

Dino Esposito

Code download available at:CuttingEdge0501.exe(161 KB)

Contents

Inspired by GridView Controls
The ABC of Controls Script Callbacks
The CallbackValidator Control
How the Control Works
Building the Control
Working Around State Issues
JavaScript Files as Embedded Resources
Conclusion

ASP.NET client callbacks represent a neat and elegant way to execute server-side code without posting and refreshing the current page. I discussed ASP.NET callbacks in the August and December 2004 installments of Cutting Edge, considering them from the perspective of rendered pages making background callbacks to the server, sending input data to the relevant page, and receiving a response. The response string can then be processed by the client however it sees fit, often manipulating the rendered page content through the Dynamic HTML (DHTML) object model and a callback JavaScript function embedded in the page.

While that use of callbacks is exciting, they can do more. A script callback mechanism can also add advanced functionalities to a server control. By implementing a couple of interfaces, any custom control can be endowed with script callback capabilities to use background round-trips to collect server data and update the user interface—the topic I'll cover this month.

Inspired by GridView Controls

If you've read my recent ASP.NET 2.0 GridView feature article, you know that the GridView control does not require you to refresh the whole page in order to display a new page of records. In fact, the GridView control provides an advanced engine for paging and sorting that is based on ASP.NET script callbacks. The data for the new page downloads behind the scenes, invisible to the user. Once it reaches the client, the data is collected by a JavaScript function and used to update the current view.

Paging and sorting callbacks are not 100 percent client-side solutions (if you need a purely client-side implementation, see the one Jeff Prosise built in the Wicked Code column in February 2004). The GridView's page and sort callbacks work on demand, downloading only the data needed; the whole data source is not downloaded onto the client. You still pay the price of a round-trip, but you're guaranteed the most current data even if that data has recently been updated on the server.

Having discovered that ASP.NET controls can support script callback capabilities had me all a-tingle for a while, and made me rush to figure out how to build my own.

Incidentally, the GridView is not the sole ASP.NET 2.0 control to sport a similar capability. Other view controls, such as TreeView, DetailsView, and FormView, provide the same feature out of the box. As a developer using a callback-enabled control, you don't need to cope with server-side code or worry about writing and embedding JavaScript code in the hosting page. The control takes care of everything and exposes an intuitive programming model through which you can control the script callback mechanism.

The ABC of Controls Script Callbacks

The ASP.NET script callback mechanism consists of two key elements: the server-side code that executes in response to a user action and the JavaScript callback code on the client that processes the results generated by the server-side event. In a scenario where a page calls back to itself, like the one I considered in the aforementioned articles, you can attach some ASP.NET-generated script code to a page button that performs a postback that's invisible to the user. Because the target of this request is the current page, the page posts to itself, similar to how it would in an ordinary postback event, though with an abbreviated page lifecycle. The page must implement the ICallbackEventHandler interface so that a method with a predefined signature can be called to generate results for the client.

So how does the scenario differ when a control triggers the out-of-band call? In this case, the target URL of the "invisible" postback is the URL of the page that hosts the caller control. The control must implement the ICallbackEventHandler in order to provide a method that generates some results for the client. Likewise, the control is responsible for injecting in the hosting page any JavaScript code that is needed to process results and refresh the page.

A callback-enabled control is simply a control that implements the ICallbackContainer and ICallbackEventHandler interfaces, both of which have one method. The ICallbackContainer interface has a method to return the script code that triggers the remote call; the ICallbackEventHandler interface provides the server-side code to execute during the call. ICallbackEventHandler is the same interface that a callback-enabled page must implement. The declaration for a sample custom control that implements callback interfaces is shown in the following code:

public class CallbackValidator : WebControl, INamingContainer, ICallbackContainer, ICallbackEventHandler

In the implementation of the ICallbackContainer interface you may need to place a call to the page's GetCallbackEventReference method to obtain a correct JavaScript call to start the server event. I'll return to this later.

The CallbackValidator Control

To understand callback-enabled server controls, let's take a look at an example of a custom validator control powered by ASP.NET script callbacks. In ASP.NET, validation controls are used to check and verify the input of the form fields defined within a Web page. The validator is a server control that inherits from the BaseValidator class which, in turn, inherits from Label.

Each validation control references an input control located elsewhere in the page. When the page is about to be submitted, the contents of any monitored server control is passed to the validator for further processing. Each validator performs a different type of verification. For example, the CompareValidator control compares the user's entry against a fixed value using a comparison operator such as less-than, equal-to, or greater-than. The RangeValidator ensures that the user's entry falls within a specified range whereas the RegularExpressionValidator validates the user's entry only if it matches a pattern defined by a regular expression.

Normally, validation takes place on the server. However, ASP.NET also provides a complete client-side implementation for most validation controls and allows the user to write custom client-side script for the rest. This allows DHTML-enabled browsers, such as Microsoft® Internet Explorer version 4.0 and later, to perform validation on the client as soon as the user tabs or clicks out of a monitored input field. In many cases, client-side validation is powerful enough to detect many significant errors and notify users. For example, a RequiredFieldValidator control verifies that the given field is not left empty. There is no need to post back to the server to verify the current value.

If client-side validation is turned on, the page won't post back until all the input fields contain valid data. In order to run secure code and to prevent malicious and underhanded attacks, you should still validate data on the server; server-side validation is always performed by the validator controls even if client-side validation is also performed. Besides, not all types of validation can be accomplished on the client. In fact, if you need to validate against a database, there's no choice but to post back to the server. And this is just where the rub lies.

A regular postback involves the page as a whole. The entire view state is uploaded, the entire page is processed, and the same large response is generated, downloaded, and rendered. Wouldn't it be nice if you could issue an out-of-band, optimized request to the server and check the state of only the controls under validation?

In ASP.NET there's no such a control. So let's write one that I'll name CallbackValidator. CallbackValidator is a custom ASP.NET 2.0 control I built to demo how a control can implement out-of-band calls to the host page and handle the event itself on the server.

When I embarked on this project, I actually had a not-so-ambitious objective: my goal was simply to modify the CustomValidator standard control. For the record, the CustomValidator control employs programmatically defined validation logic to check the validity of the user's entry. You use this approach when the values to check against are not known beforehand. The original intention of the CallbackValidator control was to offer a way to perform server-side validation without posting back the whole page. I was halfway finished with modifying my control when I realized that with no significant extra effort I could have had a custom button-like control capable of validating a number of input fields on the server without posting back the whole page. This behavior is what the CallbackValidator control is all about.

Before I delve into the control's nuts and bolts, take a look at Figure 1. The Submit button on this page just posts all the values to the server the usual way. In practice, values will be processed on the client and if all of them pass, the control passes to the server where all control input will be validated using the server-side validation code, if any. The Validate button triggers an out-of-band call to the Web server and validates only the specified input controls. When it returns, you know which values have passed validation by the server. In Figure 1, for example, you'll know whether the user ID is already taken before you try to submit the rest of the data.

Figure 1 Input Form with a Callback-Enabled Validation

Figure 1** Input Form with a Callback-Enabled Validation **

Figure 2 shows the source code of this page. As you can see, it contains an HTML server form, a few textboxes (each bound to a standard validation control), and an instance of the custom CallbackValidator control. This control is actually responsible for creating and displaying the Validate button.

Figure 2 Validation Code and Forms

<%@ Page Language="C#" MasterPageFile="~/MsdnMag.master" CompileWith="Default.aspx.cs" ClassName="Default_aspx" Title="Cutting Edge (Jan05)" %> <%@ Register TagPrefix="cc1" Namespace="MsdnMag.Controls" Assembly="CallbackControls" %> <asp:Content Runat="server" ContentPlaceHolderID="PageDescription"> <asp:Label Runat="Server" ID="Desc">Demonstrates controls that implement client-side callback functionality</asp:Label> </asp:Content> <asp:Content Runat="server" ContentPlaceHolderID="PageBody"> <h1>Register for a free account</h1> <table cellpadding="0"><tr> <td><b>User ID</b></td> <td><asp:TextBox ID="UserId" Runat="server" Text="dino" /> <asp:CustomValidator ID="valUserId" Runat="Server" ControlToValidate="UserId" ValidateEmptyText="true" ErrorMessage="The user ID already exists or is empty" OnServerValidate="EnsureUnique" Text="*" /></td> </tr> <tr> <td><b>Email address</b></td> <td><asp:TextBox ID="Email" Runat="server" /> <asp:RegularExpressionValidator ID="valEmail" Runat="Server" ControlToValidate="Email" Text="*" ErrorMessage= "The value does not appear to be a valid email address" ValidationExpression= "[a-zA-Z_0-9.-]+\@[a-zA-Z_0-9.-]+\.\w+" /></td> </tr> <tr> <td><b>Password</b></td> <td><asp:TextBox ID="Pswd" Runat="server" TextMode="Password" /> <asp:CustomValidator ID="valPswd" Runat="Server" ControlToValidate="Pswd" Text="*" ErrorMessage="Enter a strong password (8+ chars including lower and upper case, digits and symbols" OnServerValidate="EnsureStrong" /></td> </tr> <tr> <td bgcolor="lightyellow"> <cc1:CallbackValidator ID="CallbackValidator1" Runat="server" ShowDetailedInfo="true" ButtonText="Validate" /></td> <td bgcolor="lightyellow" align="right"> <asp:Button ID="Button1" Runat="server" Text="Submit" Font-Bold="True" /></td> </tr></table> </asp:Content>

How the Control Works

The CallbackValidator control inherits from WebControl and implements the INamingContainer interface. In addition, it implements the ICallbackContainer and ICallbackEventHandler interfaces for callback support.

The ICallbackContainer interface requires a method, GetCallbackScript, declared like so:

string GetCallbackScript(IButtonControl buttonControl, string argument)

GetCallbackScript takes two arguments. The first is a reference to the page's control that is expected to trigger the callback. The second argument (a string) represents any context the caller wants to pass to the method to help with the construction of the output. As the name suggests, the GetCallbackScript method prepares and returns a string with the JavaScript function call to attach to the specified button control to trigger the remote call.

The button control argument allows you to specify exactly which button in the control's UI you're making the JavaScript call for. The sample CallbackValidator control has just one clickable button; the GridView control has many, one for each link button in the pager or in the header. In ASP.NET 2.0, all controls that act like a button on a form are required to implement a new interface—IButtonControl. The interface is detailed in Figure 3 and is implemented by the following Web controls: Button, LinkButton, and ImageButton. By design, HTML button controls do not implement the interface. Note that in the Microsoft .NET Framework 1.x, the IButtonControl interface exists (albeit with a radically different set of members) only for Windows® Forms button controls.

Figure 3 IButtonControl Interface in ASP.NET 2.0

Properties
CausesValidation Indicates whether the content of the input fields should be validated when the button is clicked.
CommandArgument Gets or sets an optional parameter passed to the Command event along with the associated CommandName.
CommandName Gets or sets the command name associated with the button that is passed to the Command event.
PostBackUrl Gets or sets the URL of the page to post to from the current page when the button is clicked.
SoftkeyLabel Gets or sets the text to display for a soft key label. Ignored when the button renders to a device that does not support soft keys.
Text Gets or sets the caption of the button.
ValidationGroup Gets or sets the group of controls for which the button causes validation when it posts back to the server.
Visible Indicates whether the button is rendered on the page.
Events
Name Description
Click Occurs when the button is clicked. This event is commonly used when no command name is associated with the button.
Command Occurs when the button is clicked. The event handler receives an argument of type CommandEventArgs containing data related to this event.

The second interface required by a callback-enabled control is ICallbackEventHandler—the same interface required on pages that support script callbacks. The interface consists of one method:

string RaiseCallbackEvent(string eventArgument)

The method receives input values in the form of a string, does some server-side work, and returns its response again in the form of a string. What matters here is that both input and output data can travel packed as strings; the real content and format of the strings is up to the coder.

Before I discuss the implementation of the CallbackValidator control, take a look at Figure 4, which illustrates how the control fits in the page's HTTP handler that processes requests for ASPX resources. The CallbackValidator control looks like a button with some script code attached. The script code is just what its GetCallbackScript method returns. When clicked, the button (Validate) fires the background postback sending view state, current input values, plus a couple of custom strings named CALLBACKPARAM and CALLBACKID. The former contains the input value for RaiseCallbackEvent as created in the body of the GetCallbackScript method, and CALLBACKID serves to identify the server-side object to handle the server event. Once on the server, the page's HTTP handler that picks up the request from the ASP.NET runtime tries to locate a control with that ID that implements ICallbackEventHandler. If successful, the control's RaiseCallbackEvent method is invoked and its output returned to the client. If the CALLBACKID targets the page, the HTTP handler sees if the page implements the interface and then proceeds as usual.

Figure 4 Callback Validator Control

Building the Control

CallbackValidator is a composite control whose user interface consists of a simple pushbutton. You can easily extend this aspect of the control by adding a few properties to set the style of the button—link, push, image, or whatever. The text to be shown on the button is set by the ButtonText property. A collection property named ControlsToValidate gathers the IDs of all page validators to be tested on the server during the callback. The property is implemented as a StringCollection type and is empty at the beginning. The code in Figure 5 only lets you add control IDs at run time, which you would typically do in the Page_Load event:

void Page_Load(object sender, EventArgs e) { CallbackValidator1.ControlsToValidate.Add("valUserId"); CallbackValidator1.ControlsToValidate.Add("valEmail"); }

Figure 5 RaiseCallbackEvent

public class CallbackValidator : WebControl, INamingContainer, ICallbackContainer, ICallbackEventHandler { private StringCollection _controlsToValidate; [Description("The text to be shown on the button")] public string ButtonText { get { return Convert.ToString(ViewState["ButtonText"]); } set { ViewState["ButtonText"] = value; } } [Description("Whether to show the full validation error message")] public bool ShowDetailedInfo { get { return Convert.ToBoolean(ViewState["ShowDetailedInfo"]); } set { ViewState["ShowDetailedInfo"] = value; } } [Browsable(false)] public StringCollection ControlsToValidate { get { if (_controlsToValidate == null) _controlsToValidate = new StringCollection(); return _controlsToValidate; } } protected override void CreateChildControls() { Controls.Clear(); // Create the button to start the callback operation Button b = new Button(); b.Text = ButtonText; b.CopyBaseAttributes(this); if (ControlStyleCreated) b.ApplyStyle(ControlStyle); // Attach some script code to trigger the remote call ICallbackContainer cont = this as ICallbackContainer; b.OnClientClick = cont.GetCallbackScript((IButtonControl) b, ""); // Add the button to the control's hierarchy for display Controls.Add(b); // Inject any needed script code into the page EmbedScriptCode(); } protected override void Render(HtmlTextWriter writer) { EnsureChildControls(); base.Render(writer); } ... }

Note that the collection class doesn't persist its contents to the view state. For this reason, you must always reinitialize it when a request comes in. Note also that you should add validation controls, not input controls, to the collection. During the remote call, the CallbackValidator control will invoke the Validate method on associated controls and store the responses for the client callback. The CallbackValidator control works with existing validators making an out-of-band call to test them all; it's not actually a new type of validator itself.

As you can see in Figure 5, the CallbackValidator control creates a Button control and attaches some code to its OnClientClick property. OnClientClick is a new property introduced in ASP.NET 2.0 to add a JavaScript call to the HTML onclick event. In ASP.NET 2.0, the following two lines of code are completely equivalent:

button.Attributes["onclick"] = js; // ASP.NET 1.x; still works in 2.0 button.OnClientClick = js; // ASP.NET 2.0

The code to associate with the validate button is obtained through a specific method wrapped in the ICallbackContainer interface. Note that as of Beta 1, the use of the ICallbackContainer interface is not mandatory, but using it does help to keep code neat and clean. I didn't use it in last month's example and it is not even mentioned in the MSDN documentation for Beta 1 that discusses script callbacks. Nonetheless, ASP.NET controls that benefit from script callbacks (mostly the GridView) implement it. The only component that uses the ICallbackContainer interface is the control itself, meaning that you can easily write a callback-enabled control that doesn't use that interface.

The JavaScript function call that GetCallbackScript returns comes from the following statement:

Page.GetCallbackEventReference( this, args, "CallbackValidator_UpdateUI", "null"));

The first parameter (this) indicates the current control and sets the CALLBACKID field in the request being issued. The second parameter (args) is the input string for the server-side RaiseCallbackEvent method. The third parameter is the name of the JavaScript callback function which is used to process the output of the RaiseCallbackEvent method. Finally, the fourth parameter is null in this case, but represents a JavaScript object to be passed as the context of the callback function.

The CallbackValidator control must ensure that the JavaScript callback is defined in the hosting page and must make the decision about the format and contents of the args parameter. There is just one type of information the RaiseCallbackEvent implementation needs from its client caller: the list of validators to test. Here's the code that concatenates all validators' IDs in a pipe-separated string:

int i = 0; StringBuilder sb = new StringBuilder(""); foreach (string s in ControlsToValidate) { if (i>0) sb.Append("|"); sb.Append(s); i++; } string args = String.Format("'{0}'", sb.ToString());

An example of JavaScript code bound to the Validate button in Figure 2 might look like the following:

WebForm_DoCallback( 'ctl00$PageBody$CallbackValidator1', 'valUserId|valEmail', CallbackValidator_UpdateUI, null, null);

Note that the ID of the CallbackValidator control is mangled by the server so that it can uniquely identify every control on the page.

Working Around State Issues

As in Figure 4, the request posted back contains a few input fields. Aside from the aforementioned CALLBACKID and CALLBACKPARAM fields, the request includes a few more input fields. More precisely, it includes all the input fields in the form, plus the two specific to the callback operation. In other words, the view state is posted back along with the current values of the input fields (textboxes, dropdown lists, and so forth).

The page HTTP handler restores the view state and posted values before it checks the callback status of the request and figures out which object (Page or control) will have RaiseCallbackEvent called. In light of this, you may assume that the code defined in the RaiseCallbackEvent method will execute in a consistent and updated state. In particular, you may assume that all validators invoked by the RaiseCallbackEvent method of the CallbackValidator control will test the current values of their bound input controls. As the section title may suggest, this is not the case. Take a look at the code of RaiseCallbackEvent in Figure 6.

Figure 6 The CallbackValidator Control—Base Code

public string GetCallbackScript( IButtonControl buttonControl, string argument) { // Prepare the input for the server code int i = 0; StringBuilder sb = new StringBuilder(""); foreach (string s in ControlsToValidate) { if (i>0) sb.Append("|"); sb.Append(s); i++; } string args = String.Format("'{0}'", sb.ToString()); string js = String.Format("javascript:{0};{1};{2}; return false;", "__theFormPostData = ''", "WebForm_InitCallback()", Page.GetCallbackEventReference( this, args, "CallbackValidator_UpdateUI", "null")); return js; } public string RaiseCallbackEvent(string eventArgument) { // Execute the server-side code // Receives a string like this: // ctl|ctl|...ctl // Sends back a string like this: // ctl:valid:msg:tip|...ctl:valid:msg:tip StringBuilder sb = new StringBuilder(""); string[] controlsToValidate = eventArgument.Split('|'); int i = 0; foreach (string s in controlsToValidate) { BaseValidator val = NamingContainer.FindControl(s) as BaseValidator; if (val != null) { val.Validate(); if (i > 0) sb.AppendFormat("|"); sb.AppendFormat("{0}:{1}:{2}:{3}", val.ClientID, (val.IsValid ? "1" : "0"), (val.IsValid ? "" : GetErrorText(val)), (val.IsValid ? "" : GetTooltip(val))); } i++; } return sb.ToString(); } private void EmbedScriptCode() { Assembly dll = Assembly.GetExecutingAssembly(); StreamReader reader; reader = new StreamReader(dll.GetManifestResourceStream( "MsdnMag.CallbackValidator.js")); string js = reader.ReadToEnd(); reader.Close(); if (!Page.ClientScript.IsClientScriptBlockRegistered( "CallbackValidator")) Page.ClientScript.RegisterClientScriptBlock( typeof(CallbackValidator), "CallbackValidator", js, true); }

The method retrieves the validator control and invokes its Validate method. The method does its job (what exactly depends on the type of the validator) and sets the IsValid property to either True (valid) or False (invalid). Next, the RaiseCallbackEvent method builds its response to the client page. The return string is a pipe-separated collection of strings, each with the following form:

controlID:valid (0/1):message:tooltip

The first token is the client ID of the control that is the fully qualified ID that takes into account any mangling due to Master Pages and naming containers. The second token is 0 or 1 depending on the value of IsValid. The third token is the message the validator would display after a full postback. This corresponds to the validator's Text (default) or ErrorMessage property. I also force a * string if both are empty. By design, Text is expected to contain some text to simply mark the field as invalid. ErrorMessage, instead, provides a more detailed explanation of the error. If the CallbackValidator's ShowDetailedInfo property is true, I use the ErrorMessage string as a tooltip, as shown in Figure 7.

Figure 7 Validation Error Message

Figure 7** Validation Error Message **

So where's the state-related hassle? This machinery works great on paper, but not with real values. While debugging, I realized that the results of the validation tests were completely unreliable. For example, the User ID textbox is designed to accept anything but "Dino" and empty strings. Well, it works great with regular postbacks, but not if callback validation is used. Some well-placed breakpoints showed that all textboxes retain their original values and ignore what you may have typed before attempting to validate. The problem is not with the view state, but with posted values. The page machinery works great as usual; it just doesn't receive what I perceived to be the correct values from the client.

If you scroll the HTML source code of a page that uses ASP.NET callbacks, you see that upon page loading a call is made to a JavaScript function named WebForm_InitCallback. This function is part of the ASP.NET 2.0 infrastructure and is injected into the page through the WebResource.axd system handler. A look at the source code of this function is in order. (See last month's column for details on how to get it.) Basically, WebForm_InitCallback builds the body of the POST request when the page loads. The body of the page is a string (its name is __theFormPostData) filled with the contents of the view state and all of the input fields in the form. The code is correct; however it executes at a time I wasn't expecting! The content of the input fields is collected at load time and is not updated with user-supplied values when the post back takes place. That's why the server state appeared to be incorrect. To work around this issue, I simply repeated the call to WebForm_InitCallback before starting the out-of-band call (see Figure 6). Note that this is in fact the expected behavior rather than any sort of bug. The argument against the system calling WebForm_InitCallback before the out-of-band call is for scenarios where the user wants to perform the callback in the context of the data that was originally sent down from the server.

JavaScript Files as Embedded Resources

To top off the column, let me discuss a nifty technique that greatly simplifies JavaScript code injection in ASP.NET 2.0 custom controls. The technique also works in ASP.NET 1.x. The idea is simple: write your JavaScript code in regular JS files and add them to the project as embedded resources. (Set the Build Action property in the Visual Studio® .NET Properties window). Also, prefix the resource name with the component's namespace. Next, when you need to use the script in the code, do what the EmbedScriptCode method does in Figure 6. You use a bit of reflection, but you gain a lot in terms of code maintenance and readability.

Conclusion

ASP.NET script callbacks are an extremely powerful feature that can save time on each page update. Although a server request is issued, the whole page remains intact in the browser's window. The advantage is two-fold. First, the user has the illusion that no postback is necessary and continues reading or working with the page. Second, only a few page fragments are refreshed (using the HTML object model). In the August 2004 and December 2004 columns, I provided in-depth coverage of the basic feature. This month I demonstrated how to implement callbacks in custom controls to provide more flexibility in the controls you build.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is a Wintellect instructor and consultant based in Italy. Author of Programming ASP.NET and his newest book Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching classes on ASP.NET and ADO.NET and speaking at conferences. Get in touch at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.