Was this page helpful?
Your feedback about this content is important. Let us know what you think.
Additional feedback?
1500 characters remaining
Export (0) Print
Expand All

A Crash Course on ASP.NET Control Development: Building Callback Capabilities in ASP.NET Rich Controls

 

Dino Esposito
Solid Quality Learning

April 2006

Applies to:
   ASP.NET 2.0
   Control development

Summary: Dino Esposito uses ASP.NET Script Callbacks to create rich custom controls that retrieve data on demand from the server without erasing and reloading the current page. (16 printed pages)

Click here to download the code sample for this article.

Contents

Introduction
Core Technologies for Script Callbacks
Devising a Callback-Enabled Control
The RssReader Control
RSS Parsing
Adding Script Callbacks
Emitting Script Code
Data Exchange
Script Callback Backstage
On the Way to Atlas

Introduction

ASP.NET script callback refers to the control's ability to send separate, out-of-band HTTP requests to the server in order to retrieve fresh data. Once the data has been downloaded on the client, the control uses it to refresh its own user interface. There are two key elements in this apparently simple and ordinary description. First, a request-for-data is made instead of a more traditional request-for-page. In other words, the same page requests new data and then updates its layout. For the end user, this means no page flickering; the possibility of working while the data is retrieved and downloaded; a bit more interactivity; and a bit less of frustration. The second key factor is that you can encapsulate all the required client-side and server-side code inside the same ASP.NET control. Instantiated on the server, the ASP.NET control generates markup and ad hoc script code. As the user interacts with the control's markup elements, out-of-band calls may be issued to the server-side instance of the control to do more work and retrieve more data.

As of today, quite a few built-in ASP.NET 2.0 controls already benefit of this innovation: GridView, TreeView, and DetailsView. In particular, GridView and DetailsView take advantage of out-of-band calls for sorting and paging; the TreeView uses the additional capabilities to populate nodes when they're expanded for the first time. All these ASP.NET 2.0 controls leverage a core technology called ASP.NET Script Callbacks.

In this article, I'll use ASP.NET Script Callbacks to create rich custom controls that go down to the server to retrieve data on demand, and do all this without erasing and reloading the current page.

Core Technologies for Script Callbacks

ASP.NET Script Callbacks is not the only core technology one can use to implement calls to a remote server that don't involve the currently displayed page. Ajax.NET, for example, is a free library that is even superior to ASP.NET Script Callbacks in some respects. You can get and test Ajax.NET for ASP.NET 1.x and 2.0.

Atlas

Atlas is a package of ASP.NET client and server extensions to develop Web applications that interact with users without needing to round trip the page. There are several differences between Atlas and ASP.NET Script Callbacks and Ajax.NET.

All are core technologies to create applications that don't refresh the page to display any result. ASP.NET Script Callbacks is the official standard technology for this kind of task in ASP.NET 2.0. Ajax.NET was first created for ASP.NET 1.1 and then ported to ASP.NET 2.0. Ajax.NET has a richer programming model than ASP.NET Script Callbacks but also doesn't integrate with the postback model. With Ajax.NET you make a direct call to the server, which is faster but also jumps over page or control event handler you may have placed in the middle. And Atlas? Atlas is the future. Atlas represents a glimpse of the ASP.NET platform of the future. Compared to the other two, Atlas provides a significantly different programming model.

Devising a Callback-Enabled Control

A callback-enabled control has two main characteristics. It implements the ICallbackEventHandler interface and injects ad hoc script code in the host page to start the server call and process received data. In addition, a callback-enabled control pays due attention to its client markup so that the markup can be easily updated by using Javascript code. In practice, this means that portions of the control that may be updated over a callback must be tagged with a well-known and unique ID that works through the browser's document object model (DOM).

Not all controls are suited for a callback model. For a control with a relatively static user interface and a low level of interactivity, the script callback mechanism is likely to be overkill. On the other hand, a control the user interacts with that is expected to actively contribute to the page contents is just an excellent candidate for an upgrade toward the script callback model. Here are just a few examples of possible custom controls that might well incorporate a callback mechanism:

  • A control that validates the contents of a given input field based on server data without leaving the current page. Today, ASP.NET comes with the CustomValidator control that does the same but works over a classic postback.
  • A text box that, based on the current content, searches a server-side archive to suggest words. A similar text box is just one of the canonical examples to illustrate the benefits of callback technologies.
  • A lazy-loading list control that downloads server data and fills itself only after a client-side event. This description also fits well to controls that provide any form of contents pagination.
  • Any control that shows dynamically changing data (stock quotes, real-time data, progress bars) and that needs to refresh frequently and possibly automatically.

The control I'm going to present in this article to demonstrate the script callback mechanism roughly falls in the fourth category. It is an RSS reader that initially connects to the specified URL and displays any downloaded feeds. The user interface of the control, however, also provides a link to look for new feeds without affecting the rest of the page. Looking at Figure 1 should help you make sense of the scope of this article.

Aa479299.cccsctcall01(en-us,MSDN.10).gif

Figure 1. The RssReader Control in Action

Speaking of RSS facilities in ASP.NET, I can't help but mention the excellent work done by Dmitry Robsman that I gladly pay homage to. The work is published here: http://blogs.msdn.com/dmitryr/archive/2006/02/21/536552.aspx. Dmitry's free RSS toolkit includes support for consuming as well as publishing RSS feeds in ASP.NET applications. In particular, it includes an awesome RSS data source control to consume feeds in ASP.NET applications.

Relying on the whole arsenal of ASP.NET 2.0, writing an RSS reader solution is no longer a serious issue. For example, you can even do that in a codeless manner by combining together a XmlDataSource (or the RssDataSource control featured in the aforementioned post) and a grid or tree control in a .ascx Web user control.

The control discussed in this article is a self-contained, reusable component that doesn't require binding to other controls or data sources. All that you have to do is drop it onto a Web form and specify the desired URL. Let's delve deeper into the RssReader control.

The RssReader Control

RssReader is a composite control made of a multi-row table. The first two rows are reserved for the title and description of the RSS channel. The third row optionally contains the time the latest update occurred and a button to refresh the feed. Finally, the bottom section of the table contains one row for each downloaded post. The text of the post is directly linked to a real URL. The control also shows the time of the post. (See Figure 1.) The control's markup is generated according to the following code:

protected override void CreateChildControls()
{
    base.CreateChildControls();
    Controls.Clear();
    CreateControlHierarchy();
    ClearChildViewState();
    PrepareControlForRendering();
}
protected virtual void CreateControlHierarchy()
{
    // Build the outermost container table
    Table outer = new Table();
    outer.CellPadding = CellPadding;
    outer.CellSpacing = CellSpacing;

    // Add the header with title and description
    if (ShowHeader)
        CreateHeader(outer);

    // Add the toolbar with the Refresh link
    if (EnableRefresh)
        CreateLinks(outer);

    // Add contents
    CreateContents(outer);

    // Save the control tree
    Controls.Add(outer);
}

As you can see, quite a few properties contribute to the control's object model. They are summarized in Table 1.

Table 1. Properties of the RssReader control

PropertyDescription
CellPaddingNumber of pixels around the cells of the outermost table.
CellSpacingNumber of pixels between the cells of the outermost table.
EnableRefresh Indicates whether the RSS channel can be updated.
FeedStyleThe style used to render the feed items.
RefreshButtonTextText of the button that triggers the update.
RssFeedExact URL of the feed.
ShowHeaderIndicates whether feed title and description are displayed.
SubtitleStyleThe style used to render the feed description.
TitleStyleThe style used to render the feed title.

The URL of the feed is determined by the RssFeed string property. Unlike most commonly-used RSS aggregators, the control doesn't support any form of URL guessing. For example, you get different markup if you call http://weblogs.asp.net/despos and http://weblogs.asp.net/despos/rss.aspx. The RssReader control can successfully parse the markup you get through rss.aspx, but not the other. The same limitation applies to the aforementioned RssDataSource control and in general to any component that doesn't figure out the exact URL serving RSS feeds.

The correct algorithm to download RSS in a user-friendly way entails the following steps:

  • Make a request for any specified URL.
  • Snoop inside the downloaded source and look for a <link> tag with a type attribute of application/rss+xml.
  • Find the URL in the href attribute of the <link> tag.

For example, the http://weblogs.asp.net/despos page contains the following tag:

<link id="RSSLink" title="RSS" 
      type="application/rss+xml" 
      rel="alternate" 
      href="http://weblogs.asp.net/despos/rss.aspx">
</link>

Upon loading, the RssReader control checks the postback status of the host page. The first time the page loads, the control calls its own DataBind method. Inside the DataBind method, a channel is opened to the URL, and data is downloaded, parsed, and then displayed.

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    if (!Page.IsPostBack)
        DataBind();
}
public override void DataBind()
{
    base.DataBind();
    if (!String.IsNullOrEmpty(RssFeed))
    {
        GetFeed();
        RssInfo info = ProcessFeed();
        DisplayFeed(info);
    }
}

Note that the DataBind method is defined as a public and virtual method on the Control class. As a result, all ASP.NET controls support it without having to implement special data binding features. The RssReader control needs a method that acts as the trigger for the download. Defining a made-to-measure new method is fine, but I preferred to add some custom logic to an otherwise empty and unused DataBind method. In the end, downloading RSS feeds is a form of data binding, isn't it?

RSS Parsing

The GetFeed, ProcessFeed, and DisplayFeed methods in the preceding listing are wrappers for the methods of a download manager class, named RssHelpers. (See companion source code for details.) Downloading RSS data is pretty easy, as the following code snippet shows:

public static byte[] GetFeed(string url)
{
    WebClient req = new WebClient();
    try {
        return req.DownloadData(url);
    }
    catch { return null; }
}

The parsing of the RSS feed is minimal in the RssReader control, and is limited to extrapolating title, description, and the list of items with URL, title, and publication date. The ProcessFeed method loads the XML data in an instance of the XmlDocument class (you can use XmlTextReader to save some server memory) and uses an XPath navigator to move through it to locate desired elements. All the necessary information is finally packed into the RssInfo class, defined as follows:

internal class RssInfo
{
    public RssInfo() {
        Items = new RssItemInfoCollection();
    }
    public string Title;
    public string Description;
    public RssItemInfoCollection Items;
}
internal class RssItemInfo
{
    public string PostTitle;
    public DateTime PostDate;
    public string PostUrl;
}
internal class RssItemInfoCollection : Collection<RssItemInfo> {}

All the information about downloaded posts is formatted as a table and inserted into the main table. The final result is what you saw in Figure 1.

It is interesting to note that the screenshot of Figure 1 is taken from within Visual Studio 2005; that is, with the RssReader control working at design-time. Nonetheless, the control shows real data as long as Internet connection is available. You should use a custom control designer to gracefully handle lack of connection and still make the control show a readable and user-friendly user interface. (See Figure 2.)

Aa479299.cccsctcall02(en-us,MSDN.10).gif

Figure 2. The RssReader control at design time when no connection is available

Adding Script Callbacks

As mentioned, ASP.NET custom controls can optionally support script callbacks. In practice, support for script callbacks means the ability to place calls to a server-side instance of the control in order to execute a particular piece of code. A control that support callbacks is capable of the following actions:

  • Make an HTTP request in response to a client-side event ( a button click, a mouse move, a timer tick).
  • The target of such an HTTP request is the same page that contains the control. Directed at an .aspx resource, the request passes through the same pipeline of modules as any other requests made by the browser.
  • The request is mapped to a page HTTP handler and goes through the usual page lifecycle up to a certain point—right before the PreRenderComplete event. It is important to note that all page events in the range Init-PreRender are regularly fired.
  • After the PreRender event, the page processes the callback invocation. The internal page code detects the target of the callback by ID. In this case, it will be a particular instance of the RssReader control located in the page.
  • By design, the control implements the ICallbackEventHandler interface. The page code just invokes the methods on the interface.
  • The result of the server-side code serialized to a string is sent back to the browser as the HTTP response.

The client-side infrastructure of script callbacks captures the HTTP response and passes it to a Javascript function provided by the control—in this case the RssReader control. The Javascript code processes the response and uses the browser's DOM to update the current page. Let's see how to extend the code of the RssReader control and what kind of Javascript code the RssReader control should emit in the ASP.NET client page.

The first step to adding script callback capabilities to RssReader is implementing the ICallbackEventHandler interface.

void ICallbackEventHandler.RaiseCallbackEvent(string argument)
{
    // No input data to process; just returns
    return;
}
string ICallbackEventHandler.GetCallbackResult()
{
    // Get the feed
    GetFeed();

    // Process the feed
    RssInfo info = ProcessFeed();

    // Prepare the return value for the client
    return RssHelpers.SerializeFeed(info);
}

In most cases, you want to execute server-side code in response to client-side actions and passing some input parameters. For example, the GridView control passes the index of the requested page when it implements paging through script callbacks. In this case, the RssReader control doesn't need any input data. The RaiseCallbackEvent method is the entry point in the server-side infrastructure for script callbacks. In the implementation, you typically cache the argument for later use. Note that the argument is always a string. To pass multiple arguments, figure out a serialization algorithm that puts all the data you need in a single string. For example, you can build a |-separated string in which each token correspond to a different piece of data. The algorithm you use is arbitrary. Unlike Ajax.NET and Atlas, ASP.NET Script Callback provides no serialization facilities.

The GetCallbackResult method does the requested server-side work and prepares the return value for the client. What the GetCallbackResult method returns becomes the input value for the Javascript function to update the client page.

The ASP.NET Script Callback mechanism also requires a script-based infrastructure on the client to start and finalize the out-of-band call. The RssReader is charged with the task of emitting this Javascript code.

Emitting Script Code

The Refresh link button (see Figure 1) will trigger the script callback. Hence, it must be bound to some specific Javascript code. The table row that contains the link is divided in two cells. The left cell is for the time of the last update; the right cell is for the link.

TableCell refreshCell = new TableCell();
refreshCell.HorizontalAlign = HorizontalAlign.Right;
toolbar.Cells.Add(refreshCell);
_buttonRefresh = new HtmlAnchor();
refreshCell.Controls.Add(_buttonRefresh);
_buttonRefresh.InnerText = RefreshButtonText;
_buttonRefresh.HRef = "javascript:RefreshFeed()";
if (!Page.ClientScript.IsStartupScriptRegistered(
      this.GetType(), "RefreshFeed")) {
    string js = GetRefreshFeedScript();
    Page.ClientScript.RegisterClientScriptBlock(
         this.GetType(), "RefreshFeed", js, true);
}

The refresh button is bound to the RefreshFeed Javascript function. Where's this function defined? The source of the function is automatically generated on the server by the helper GetRefreshFeedScript function. Next, the string that contains the Javascript code is emitted to the page. More precisely, the Javascript string is registered as a client script block with the page. Any piece of script registered with the page is then emitted inside a proper, client-side <script> block. In this way, the RssReader control guarantees that RefreshFeed is defined in the page and called when the link button is clicked. Here's the source code of RefreshFeed.

function RefreshFeed() 
{
    var title = document.getElementById("RssReader1_RssTitle");
    title.innerHTML = 'Loading ...';
    WebForm_DoCallback('RssReader1', 0, UpdateFeed, null, null, false);
}

The function first retrieves a client reference to the first (and unique) cell in the topmost row. Next, it sets the inner text to "Loading…" and starts the callback. (See Figure 3.)

Aa479299.cccsctcall03(en-us,MSDN.10).gif

Figure 3. The RssReader control is refreshing

It is important to note that WebForm_DoCallback is an opaque function that is emitted by ASP.NET and wouldn't be so clearly visible to developers and users if Javascript was only a compiled language. The call to WebForm_DoCallback is generated by the following code when invoked from within the GetRefreshFeedScript helper.

Page.ClientScript.GetCallbackEventReference(
    this, "0", "UpdateFeed", null))

What's UpdateFeed? It is another Javascript function that RssReader is responsible for emitting that will take care of the return value of the callback to update the page. Looking at Figure 3, UpdateFeed sets title, description, date, and items of the RSS control at the end of the refresh operation.

UpdateFeed needs to retrieve key HTML elements to update with fresh data. How is this possible? When you design the user interface of the callback-enabled control, pay attention to associate HTML elements that represent updatable elements with a known ID. As an example, let's take a look at the code that creates the header of the RssReader.

protected virtual void CreateHeader(Table parent)
{
    // Add the title row
    TableRow rowTitle = new TableRow();
    parent.Rows.Add(rowTitle);
    _title = new TableCell();
    _title.ColumnSpan = 2;
    _title.ID = TitleID;
    rowTitle.Cells.Add(_title);

    // Add the subtitle row
    TableRow rowSubTitle = new TableRow();
    parent.Rows.Add(rowSubTitle);
    _subtitle = new TableCell();
    _subtitle.ColumnSpan = 2;
    _subtitle.ID = DescriptionID;
    rowSubTitle.Cells.Add(_subtitle);
}

As you can see, the two instructions rendered with boldface set the ID of the two table cells whose content indicates title and subtitle of the RSS feed. TitleID and DescriptionID are string constants used to name the HTML elements and to emit proper Javascript code to script them on the client.

Data Exchange

When you set up a script callback mechanism, you typically send input parameters to the server and receive a return value. ASP.NET Script Callback only allow these parameters to be a string. Any other type, including containers of a variety of data, must be serialized to a string.

As far as input is concerned, note that if you want to pass the current value of an input field you should use the Javascript expression that retrieves it. You can't rely on the ASP.NET postback mechanism. The ASP.NET postback mechanism still posts the value of any input fields in the page, but it forwards the "last known good state"—that is, the values of the input fields when the page was displayed to the user. In other words, if you read the value of, say, a TextBox on the server during a callback, well, that value doesn't reflect any changes the user may have entered before starting the callback.

For example, if you have a text box and want to execute a callback passing the current contents, then generate the call to WebForm_DoCallback as follows:

Page.ClientScript.GetCallbackEventReference(
    this, 
    "document.getElementById(\"TextBox1\").value", 
    "UpdateFeed", 
    null))

The second argument to GetCallbackEventReference is the Javascript expression that retrieves the desired value.

Once the server operation has completed, how would you pack any results for the client? If the return value can be expressed as a string, you're all set. If you have multiple values, you can create a comma-separated string. I'll consider a slightly smarter approach here.

The following method takes the object that contains the newly downloaded feeds and prepares a string to be uploaded to the client.

public static string SerializeFeed(RssInfo info)
{
    StringBuilder sb = new StringBuilder();
    sb.Append("function makeRssObj() { ");
    sb.AppendFormat("this.Title = \"{0}\"; ", info.Title);
    sb.AppendFormat("this.Desc = \"{0}\"; ", info.Description);
    sb.AppendFormat("this.Updated = \"{0}\"; ", 
            DateTime.Now.ToString(RssReader.LastUpdatedFormat));
    sb.AppendFormat("this.Items = \"{0}\"; ", 
            RssHelpers.FormatFeed(info)); 
    sb.Append("};");
    return sb.ToString();
}

As you can see, the SerializeFeed method creates a Javascript function that fully describes an object.

function makeRssObj()
{
   this.Title = "title of the feed";
   this.Description = "description of the feed";
   this.Updated = "current date";
   this.Items = "html markup for the feed items";
}

The string that represents this function is uploaded as the response of the callback request. This string becomes the input of the UpdateFeed, specifically its first argument.

function UpdateFeed(response, context) {
  eval(response);
  var feedInfo = new makeRssObj();
  var title = document.getElementById("RssReader1_RssTitle");
  title.innerHTML = feedInfo.Title;
  var subtitle = document.getElementById("RssReader1_RssDescription");
  subtitle.innerHTML = feedInfo.Desc;
  var feed = document.getElementById("RssReader1_RssContents");
  feed.innerHTML = feedInfo.Items;
  var now = document.getElementById("RssReader1_RssLastUpdated");
  now.innerHTML = feedInfo.Updated;
}

The string is first evaluated to obtain a callable object and then instantiated and consumed.

Script Callback Backstage

Whether you use the built-in ASP.NET 2.0 Script Callbacks, Ajax.NET (or a commercial analogous library), or just jumped on the Atlas bandwagon, there are a few fast facts that you must be aware of:

  • The world of Web is changing . Perhaps due to a fortunate astral conjunction, more than 90 percent of the browsers available today feature advanced capabilities as far as XML over HTTP and Dynamic HTML are concerned. Why not taking advantage of this?
  • Any devised or devisable script callback technology is based on a core component that, at least in the Windows world, is hanging around since 1998—the XmlHttpRequest object.
  • The XmlHttpRequest object was first introduced to support Web access capabilities of Microsoft Exchange and then bundled with Internet Explorer 5.0. Immediately reinvention for the Mozilla platform is today a de facto standard. It is key to note that in the next version of Internet Explorer (7.0) the object will also be exposed as part of the browser's DOM, whereas today it requires that you enable ActiveX scripting.
  • Ajax is an acronym for Asynchronous Javascript and XML and indicates a bunch of technologies (first and foremost XmlHttpRequest) that enable script callbacks. The so-called Ajax (programming) lifestyle indicates Web applications that employ a core technology for page updates with round tripping the whole page.
  • By the way, by embracing the Ajax lifestyle you don't save yourself any of the required server roundtrips. Simply, these roundtrips occur silently and transparently without affecting the user interface and without page flickering. Not too bad, actually.

I implemented a real solution using ASP.NET Script Callbacks. What's the difference between this and Ajax.NET or Atlas?

As mentioned, the core technologies remain the same. They all reduce to sending a HTTP command to the same URL of the current page. With ASP.NET Script Callback, the request is processed through the same pipeline as regular browser requests. Ajax.NET installs a custom HTTP handler, and filters and processes its own requests. Atlas, in turn, adds a bunch of script code and installs an HTTP module on the server to hook up requests. It should be noted that Ajax.NET is a technology available also for ASP.NET 1.1.

On the Way to Atlas

Even though ASP.NET Script Callbacks is the official ASP.NET 2.0 way to the Ajax lifestyle (and an effective one indeed), overall I tend to consider it a technology that will be soon replaced by Atlas.

I'll start covering Atlas in future articles, but for the time being it would be interesting to see at some advantages that Atlas could bring to this RssReader control.

The essential factor is that Atlas comes with a rich client-side object model, making it possible for everybody to do some good work on the client; that is, from within the UpdateFeed Javascript callback. In other words, RssReader goes to the server, orders a new RSS download, formats data for display on the server, and returns a HTML ready-to-display string. On the client, the string is simply injected in the existing page DOM.

Obviousy, in this way you move much more data than needed across the wire. This is inevitable today because there's not much programming power in the standard Javascript. It's hard (to say the least) today to parse an XML document or build complex string layouts using Javascript. You would need, for example, an XML parser and a StringBuilder class written in Javascript!

Guess what; these are just two of the classes that Atlas injects into your client pages.

Looking at possible evolutions of the RssReader control in light of Atlas, there's a second, subtler aspect to consider. Atlas also makes available a client-side data binding model and an engine to make direct calls to a public URL. By combining these elements together, one could completely re-architect the RssReader control to avoid roundtrips. The control might use Javascript to open an RSS channel, download, parse, and display data—all from within the client with no postback whatsoever, neither classic nor through XmlHttpRequest.

The new era of the Web has just begun.

 

About the author

Dino Esposito is a Solid Quality Learning mentor and the author of Programming Microsoft ASP.NET 2.0 (Microsoft Press, 2005). Based in Italy, Dino is a frequent speaker at industry events worldwide. Get in touch at cutting@microsoft.com or join the blog at http://weblogs.asp.net/despos.

Show:
© 2015 Microsoft