Chapter 14. Ajax and Client Scripting

ASP.NET MVC is first and foremost a server-side technology. It's an extremely flexible framework for handling HTTP requests and generating HTML responses. But HTML alone is static—it only updates each time the browser loads a new page—so by itself it can't deliver a rich interactive user experience. To manipulate the HTML document object model (DOM) directly in the browser, or to break free of the traditional full-page request-response cycle, you need some kind of programming technology that runs inside the browser (i.e., on the client side).

There's never been a shortage of client-side technologies. For example, we've had JavaScript, Flash, Flex, Air, VBScript, ActiveX, Java applets, HTC files, XUL, and of course Silverlight. In fact, there have been so many incompatible technologies, each of which may or may not be available in any given visitor's browser, that for many years the whole situation was stalled. Most web developers fell back on the safe option of using no client-side scripting at all, even though HTML alone delivers a mediocre user experience by comparison to desktop (e.g., Windows Forms or WPF) applications.

No wonder web applications got a reputation for being clunky and awkward. But around 2004, a series of high-profile web applications appeared, including Google's Gmail, which made heavy use of JavaScript to deliver an impressively fast, desktop-like UI. These applications could respond quickly to user input by updating small subsections of the page (instead of loading an entirely new HTML document), using a technique that came to be known as Ajax.[83] Almost overnight, web developers around the world realized that JavaScript was powerful and (almost always) safe to use.

Why You Should Use a JavaScript Toolkit

If only that was the end of our troubles! What's not so good about JavaScript is that every browser still exposes a slightly different API. Plus, as a truly dynamic programming language, JavaScript can be baffling to C# programmers who think in terms of object types and expect full IntelliSense support.

Basically, JavaScript and Ajax require hard work. To take the pain away, you can use a third-party JavaScript toolkit, such as jQuery, Prototype, MooTools, or Rico, which offer a simple abstraction layer to accomplish common tasks (e.g., asynchronous partial page updates) without all the fiddly work. Of these, jQuery has gained a reputation as being perhaps the finest gift that web developers have ever received, so much so that Microsoft now ships it with ASP.NET MVC.

Some ASP.NET developers still haven't caught up with this trend, and still avoid JavaScript toolkits or even JavaScript entirely. In many cases, that's because it's very hard to integrate traditional Web Forms with most third-party JavaScript libraries. Web Forms' notion of postbacks, its complicated server-side event and control model, and its tendency to generate unpredictable HTML all represent challenges. Microsoft addressed these by releasing its own Web Forms-focused JavaScript library, ASP.NET AJAX.

In ASP.NET MVC, those challenges simply don't exist, so you're equally able to use any JavaScript library (including ASP.NET AJAX if you want). Your options are represented by the boxes in Figure 14-1.

Options for Ajax and client scripting in ASP.NET MVC

Figure 14-1. Options for Ajax and client scripting in ASP.NET MVC

In the first half of this chapter, you'll learn how to use ASP.NET MVC's built-in Ajax.* helper methods, which deal with simple Ajax scenarios. In the second half of the chapter, you'll learn how you can use jQuery with ASP.NET MVC to build sophisticated behaviors while retaining support for the tiny minority of visitors whose browsers don't run JavaScript.

ASP.NET MVC's Ajax Helpers

The MVC Framework comes with a few HTML helpers that make it very easy to perform asynchronous partial page updates:

  • Ajax.ActionLink() renders a link tag, similar to Html.ActionLink(). When clicked, it fetches and injects new content into the existing HTML page.

  • Ajax.BeginForm() renders an HTML form, similar to Html.BeginForm(). When submitted, it fetches and injects new content into the existing HTML page.

  • Ajax.RouteLink() is the same as Ajax.ActionLink(), except that it generates a URL from an arbitrary set of routing parameters, not necessarily including one called action. This is the Ajax equivalent of Html.RouteLink(). It's mostly useful in advanced scenarios where you're targeting a custom IController that might not have any concept of an action method. Its usage is otherwise identical to Ajax.ActionLink(), so I won't mention it again.

  • Similarly, Ajax.BeginRouteForm() is the same as Ajax.BeginForm(), except that it generates a URL from an arbitrary set of routing parameters, not necessarily including one called action. This is the Ajax equivalent of Html.BeginRouteForm(). Its usage is otherwise identical to Ajax.BeginRouteForm(), so I won't mention it again.

These .NET methods are wrappers around functionality in Microsoft's ASP.NET AJAX library, so they will work on most modern browsers,[84] assuming JavaScript is enabled. The helpers merely save you the trouble of writing JavaScript and figuring out the ASP.NET AJAX library.

Note that your view pages all have access to a property called Ajax of type System.Web.Mvc.AjaxHelper. The helper methods, such as ActionLink(), aren't defined directly on the AjaxHelper type: they are in fact extension methods on the AjaxHelper type. These extension methods are actually defined and implemented in a static type called AjaxExtensions in the System.Web.Mvc.Ajax namespace. So, you can add your own custom Ajax.* helpers (just add more extension methods on AjaxHelper). You can even replace the built-in ones completely by removing Web.config's reference to System.Web.Mvc.Ajax. It's exactly the same as how you can add to or replace the Html.* helpers.

Fetching Page Content Asynchronously Using Ajax.ActionLink

Before you can use these helpers, your HTML pages must reference two JavaScript files. One is specific to ASP.NET MVC's Ajax.* helpers; the other is the ASP.NET AJAX library upon which they rely. Both files are present by default in the /Scripts folder of any new ASP.NET MVC 2 project, but you still need to reference them by adding <script> tags somewhere in your view or master page—for example:

<html>
  <body>
    <!-- Rest the page goes here -->

    <script type="text/javascript"
            src="<%: Url.Content("~/Scripts/MicrosoftAjax.js") %>"></script>
    <script type="text/javascript"
            src="<%: Url.Content("~/Scripts/MicrosoftMvcAjax.js") %>"></script>
  </body>
</html>

Tip

A few years ago, most web developers referenced external JavaScript files by placing <script> tags in the <head> section of their HTML pages. However, the current recommendation for best performance is, where possible, to reference external JavaScript files using <script> tags at the bottom of your HTML page, so that the browser can render the page without blocking parallel HTTP downloads. This is perfectly legal in HTML, and works fine as long as you don't try to reference any of the script's objects or functions from other <script> tags earlier in the document. For more details, see http://developer.yahoo.com/performance/rules.html#js_bottom.

Notice the use of Url.Content() to reference the scripts by their application-relative virtual paths (i.e., ~/path). If you reference your static resources this way, they'll keep working even if you deploy to a virtual directory.

In a moment, I'll document the Ajax.ActionLink() method in detail. But first, let's see it in action. Check out the following view fragment:

<h2>What time is it?</h2>
<p>
    Show me the time in:
<%: Ajax.ActionLink("UTC", "GetTime", new { zone = "utc" },
                    new AjaxOptions { UpdateTargetId = "myResults" }) %>
<%: Ajax.ActionLink("BST", "GetTime", new { zone = "bst" },
                    new AjaxOptions { UpdateTargetId = "myResults" }) %>
<%: Ajax.ActionLink("MDT", "GetTime", new { zone = "mdt" },
                    new AjaxOptions { UpdateTargetId = "myResults" }) %>
</p>
<div id="myResults" style="border: 2px dotted red; padding: .5em;">
    Results will appear here
</div>
<p>
    This page was generated at <%: DateTime.UtcNow.ToString("h:MM:ss tt") %> (UTC)
</p>

Each of the three Ajax links will request data from an action called GetTime (on the current controller), passing a parameter called zone. The links will inject the server's response into the div called myResults, replacing its previous contents.

Right now, if you click those links, nothing at all will happen. The browser will issue an asynchronous request, but there isn't yet any action called GetTime, so the server will say "404 Not Found." (No error message will be displayed, however, because the Ajax.* helpers don't display error messages unless you tell them to do so.) Make it work by implementing a GetTime() action method as follows:

public string GetTime(string zone)
{
    DateTime time = DateTime.UtcNow.AddHours(offsets[zone]);
    return string.Format("<div>The time in {0} is {1:h:MM:ss tt}</div>",
                         zone.ToUpper(), time);
}

private Dictionary<string, int> offsets = new Dictionary<string, int> {
    { "utc", 0 }, { "bst", 1 }, { "mdt", −6 }
};

Notice that there's nothing special about this action method. It doesn't need to know or care that it's servicing an asynchronous request—it's just a regular action method. If you set a breakpoint inside the GetTime() method and then run your application with the Visual Studio debugger, you'll see that GetTime() is invoked (to handle each asynchronous request) exactly like any other action method.

For simplicity, this action method returns a raw string. It's also possible to render a partial view, or do anything else that results in transmitting text back to the browser. Whatever you send back from this action method, the Ajax.ActionLink() links will insert it into the current page, as shown in Figure 14-2.

Ajax.ActionLink() inserts the response into a DOM element.

Figure 14-2. Ajax.ActionLink() inserts the response into a DOM element.

That was easy! Notice that the host page remained constant (the timestamp at the bottom didn't change). You've therefore done a partial page update, which is the key trick in Ajax.

Warning

If the browser doesn't have JavaScript enabled, then the links will behave as regular links (as if you'd generated them using Html.ActionLink()). That means the entire page will be replaced with the server's response, as in traditional web navigation. Sometimes that behavior is what you want, but more often it isn't. Later in this chapter, you'll learn a technique called progressive enhancement, which lets you retain satisfactory behavior even when JavaScript isn't enabled.

Passing Options to Ajax.ActionLink

Ajax.ActionLink() has numerous overloads. Most of them correspond to the various overloads of Html.ActionLink(), since the different combinations of parameters just give you different ways of generating a target URL from routing parameters. The main difference is that you must also supply a parameter of type AjaxOptions, which lets you configure how you want the asynchronous link to behave. The available options are listed in Table 14-1.

Table 14-1. Properties Exposed by AjaxOptions

Property

Type

Meaning

Confirm

string

If specified, the browser will pop up an OK/Cancel prompt displaying your message. The asynchronous request will only be issued if the user clicks OK. Most people use this to ask, "Are you sure you wish to delete the record {name}?" (which is lazy, since OK and Cancel don't really make sense as answers[a]).

HttpMethod

string

This specifies which HTTP method (e.g., GET or POST) the asynchronous request should use. The default is POST. You're not limited to GET and POST; you can use other HTTP methods such as PUT or DELETE if you think they describe your operations more meaningfully (and technically, you can even make up your own method names, though I'm not sure why you'd want to). If you use something other than GET or POST, then MicrosoftMvcAjax.js will actually issue a POST request but also send an X-HTTP-Method-Override parameter specifying your desired method. This is to ensure that all browsers will be able to issue the request. You can learn about how ASP.NET MVC will respect the X-HTTP-Method-Override parameter by reading the "Overriding HTTP Methods to Support REST Web Services" section in Chapter 10.

InsertionMode

InsertionMode (enum)

This specifies whether to replace the target element's existing content (Replace, which is the default) or add the new content at the element's top (InsertBefore) or bottom (InsertAfter).

LoadingElementId

string

If specified, the HTML element with this ID will be made visible (via a CSS rule similar to display:block, depending on the element type) when the asynchronous request begins, and will then be hidden (using display:none) when the request completes. To display a "Loading . . ." indicator, you could place a spinning animated GIF in your master page, initially hidden using the CSS rule display:none, and then reference its ID using LoadingElementId.

OnBegin

string

This specifies the name of a JavaScript function that will be invoked just before the asynchronous request begins. You can cancel the asynchronous request by returning false. More details follow.

OnComplete

string

This specifies the name of a JavaScript function that will be invoked when the asynchronous request completes, regardless of whether it succeeds or fails. Details follow.

OnSuccess

string

This specifies the name of a JavaScript function that will be invoked if the asynchronous request completes successfully. This happens after OnComplete. Details follow.

OnFailure

string

This specifies the name of a JavaScript function that will be invoked if the asynchronous request completes unsuccessfully (e.g., if the server responds with a 404 or 500 status code). This happens after OnComplete. Details follow.

UpdateTargetId (required)

string

This specifies the ID of the HTML element into which you wish to insert the server's response.

Url

string

If specified, the asynchronous request will be issued to this URL, overriding the URL generated from your routing parameters. This gives you a way to target different URLs depending on whether JavaScript is enabled (when JavaScript isn't enabled, the link acts as a regular HTML link to the URL generated from the specified routing parameters). Note that for security reasons, browsers do not permit cross-domain Ajax requests, so you can still only target URLs on your application's domain. If you need to target a URL on a different domain, see the coverage of jQuery and JSONP later in this chapter.

[a] Recently I used a web application that asked, "Are you sure you wish to cancel this job? OK/Cancel." Unfortunately, there's no straightforward way to display a prompt with answers other than OK and Cancel. This is a browser limitation. A possible workaround is to use the jQuery UI Dialog component available from http://jqueryui.com/demos/dialog/.

Running JavaScript Functions Before or After Asynchronous Requests

You can use OnBegin, OnComplete, OnSuccess, and OnFailure to intercept different points around an asynchronous request. The sequence goes as follows: OnBegin, then OnComplete, and then either OnSuccess or OnFailure. You can abort this sequence by returning false from OnBegin or OnComplete. If you return anything else (or don't return anything at all), your return value will simply be ignored and the sequence will proceed.

When any of the four functions are invoked, they receive a single parameter that describes everything that's happening. For example, to display an error message on failure, you can write the following:

<script type="text/javascript">
    function handleError(ajaxContext) {
        var response = ajaxContext.get_response();
        var statusCode = response.get_statusCode();
        alert("Sorry, the request failed with status code " + statusCode);
    }
</script>
<%: Ajax.ActionLink("Click me", "MyAction",
    new AjaxOptions { UpdateTargetId = "myElement", OnFailure = "handleError"}) %>

The ajaxContext parameter exposes the following functions, which you can use to retrieve more information about the asynchronous request context (see Table 14-2).

Table 14-2. Functions Available on the Parameter Passed into the OnBegin, OnComplete, OnSuccess, and OnFailure Handlers

Method

Return Value

Return Value Type

get_data()

The full HTML of the server's response (if there was a response)

String

get_insertionMode()

The InsertionMode option used for this Ajax.ActionLink()

0, 1, or 2 (meaning Replace, InsertBefore, or InsertAfter, respectively).

get_loadingElement()

The HTML element corresponding to LoadingElementId

DOM element

get_object()

A JavaScript object obtained by deserializing the JSON (JavaScript Object Notation) value returned by the server (e.g., if you call an action that returns JsonResult)

Object

get_request()

The outgoing request

ASP.NET AJAX's Sys.Net.WebRequest type (see the ASP.NET AJAX documentation for full details)

get_response()

The server's response

ASP.NET AJAX's Sys.Net.WebRequestExecutor type (see the ASP.NET AJAX documentation for full details)

get_updateTarget()

The HTML element corresponding to UpdateTargetId

DOM element

Detecting Ajax Requests

I mentioned earlier that Ajax.ActionLink() can fetch HTML from any action method, and the action method doesn't need to know or care that it's servicing an Ajax request. That's true, but sometimes you do care whether or not you're servicing an Ajax request. You'll see an example of this later in the chapter when reducing the bandwidth consumed by Ajax requests.

Fortunately, it's easy to determine, because each time MicrosoftMvcAjax.js issues an Ajax request, it adds a special request parameter called X-Requested-With with the value XMLHttpRequest. It adds this key/value pair to the HTTP headers collection (i.e., Request.Headers), plus either the POST payload (i.e., Request.Form) or the query string (i.e., Request.QueryString), depending on whether it's sending a POST or a GET request.

The easiest way to detect it is simply to call IsAjaxRequest(), an extension method on HttpRequestBase.[85] Here's an example:

public ActionResult GetTime(string zone)
{
    DateTime time = DateTime.UtcNow.AddHours(offsets[zone]);

    if(Request.IsAjaxRequest()) {
        // Produce a fragment of HTML
        string fragment = string.Format(
            "<div>The time in {0} is {1:h:MM:ss tt}</div>", zone.ToUpper(), time);
        return Content(fragment);
    }
    else {
        // Produce a complete HTML page
        return View(time);
    }
}

This is one way of retaining useful behavior for browsers that don't have JavaScript enabled, since they will replace the entire page with the response from your method. I'll discuss a more sophisticated approach later in this chapter.

Submitting Forms Asynchronously Using Ajax.BeginForm

Sometimes you might want to include user-supplied data inside an asynchronous request. For this, you can use Ajax.BeginForm(). It takes roughly the same parameters as Html.BeginForm(), plus an AjaxOptions configuration object as documented previously in Table 14-1.

For example, you could update the previous example's view as follows:

<h2>What time is it?</h2>
<% using(Ajax.BeginForm("GetTime",
                        new AjaxOptions { UpdateTargetId = "myResults" })) { %>
   <p>
        Show me the time in:
        <select name="zone">
            <option value="utc">UTC</option>
            <option value="bst">BST</option>
            <option value="mdt">MDT</option>
        </select>
        <input type="submit" value="Go" />
   </p>
<% } %>
<div id="myResults" style="border: 2px dotted red; padding: .5em;">
    Results will appear here
</div>
<p>
    This page was generated at <%: DateTime.UtcNow.ToString("h:MM:ss tt") %> (UTC)
</p>

Without changing the GetTime() action method in any way, you'd immediately have created the UI depicted in Figure 14-3.

Ajax.BeginForm() inserts the response into a DOM element.

Figure 14-3. Ajax.BeginForm() inserts the response into a DOM element.

There isn't much more to say about Ajax.BeginForm(), because it's basically just what you'd get if you crossbred an Html.BeginForm() with an Ajax.ActionLink(). All its configuration options are what it inherits from its parents.

Asynchronous forms work especially nicely for displaying search results without a full-page refresh, or for making each row in a grid separately editable.

Invoking JavaScript Commands from an Action Method

You may remember from Chapter 9 that ASP.NET MVC includes an action result type called JavaScriptResult. This lets you return a JavaScript statement from your action method. ASP.NET MVC's built-in Ajax.* helpers are programmed to notice when you've done this,[86] and they'll execute your JavaScript statement rather than inserting it as text into the DOM. This is useful when you have taken some action on the server, and you want to update the client-side DOM to reflect the change that has occurred.

For example, consider the following snippet of a view. It lists a series of items, and next to each is a "delete" link implemented using Ajax.ActionLink(). Notice that the last parameter passed to Ajax.ActionLink() is null—it isn't necessary to specify an AjaxOptions value when using JavaScriptResult. This produces the output shown in Figure 14-4.

<h2>List of items</h2>
<div id="message"></div>
<ul>
    <% foreach (var item in Model) { %>
        <li id="item_<%: item.ItemID %>">
            <b><%: item.Name %></b>
            <%: Ajax.ActionLink("delete", "DeleteItem", new {item.ItemID}, null) %>
        </li>
    <% } %>
</ul>
<i>Page generated at <%: DateTime.Now.ToLongTimeString() %></i>
A series of links that invoke Ajax requests

Figure 14-4. A series of links that invoke Ajax requests

When the user clicks a "delete" link, it will asynchronously invoke an action called DeleteItem, passing an itemID parameter. Your action method should tell your model layer to delete the requested item, and then you might want the action method to instruct the browser to update its DOM to reflect this. You can implement DeleteItem() along the following lines:

[HttpPost] // Only allow POSTs (this action causes changes)
public JavaScriptResult DeleteItem(int itemID)
{
    var itemToDelete = GetItem(itemID);
    // To do: Actually instruct the model layer to delete "itemToDelete"

    // Now tell the browser to update its DOM to match
    var script = string.Format("OnItemDeleted({0}, {1})",
                               itemToDelete.ItemID,
                               JavaScriptEncode(itemToDelete.Name));
    return JavaScript(script);
}

private static string JavaScriptEncode(string str)
{
    // Encode certain characters, or the JavaScript expression could be invalid
    return new JavaScriptSerializer().Serialize(str);
}

The key point to notice is that by calling JavaScript(), you can return a JavaScript expression—in this case, an expression of the form OnItemDeleted(25, "ItemName")—and it will be executed on the client. Of course, this will only work once you've defined OnItemDeleted() as follows:

<script type="text/javascript">
    function OnItemDeleted(id, name) {
        document.getElementById("message").innerHTML = name + " was deleted";
        var deletedNode = document.getElementById("item_" + id);
        deletedNode.parentNode.removeChild(deletedNode);
    }
</script>

This creates the behavior depicted in Figure 14-5.

Each click causes the browser to fetch and execute a JavaScript command from the server.

Figure 14-5. Each click causes the browser to fetch and execute a JavaScript command from the server.

While it might seem convenient to use JavaScriptResult in this way, you should think carefully before using it widely. Embedding JavaScript code directly inside an action method is akin to embedding a literal SQL query or literal HTML inside an action method: it's an uncomfortable clash of technologies. Generating JavaScript code using .NET string concatenations is brittle and tightly couples your server-side code to your client-side code.

As a tidier alternative, you can return a JsonResult from the action method and use jQuery to interpret it and update the browser's DOM. This eliminates both the tight coupling and the string encoding issues. You'll see how to do this later in the chapter.

Reviewing ASP.NET MVC's Ajax Helpers

As you've seen from the preceding examples, the Ajax.* helpers are extremely easy to use. They don't usually require you to write any JavaScript, and they automatically respect your routing configuration when generating URLs. Often, Ajax.ActionLink() is exactly what you need for a simple bit of Ajax, and it gets the job done immediately with no fuss—very satisfying!

But sometimes you might need something more powerful, becausethe Ajax.* helpers are limited in the following ways:

  • They only do simple page updates. On their own, they can inject a finished block of HTML into your existing page, but if you want to receive and process raw data (e.g., data in JSON format), or if you want to customize how it manipulates your DOM, you'll have to write an OnSuccess handler in JavaScript. And if you're going to write your own JavaScript, you might as well do it the easy way with jQuery.

  • When updating your DOM, they simply make elements appear or disappear. There's no built-in support for making things fade or slide out, or performing any other fancy animation.

  • The programming model doesn't naturally lend itself to retaining useful behavior when JavaScript is disabled.

To overcome these limitations, you can write your own raw JavaScript (and deal with its compatibility issues manually) or make use of a full-fledged JavaScript library.

For example, you could directly use Microsoft's ASP.NET AJAX library. However, ASP.NET AJAX is a heavyweight option: its main selling point is its support for ASP.NET Web Forms' complicated server-side event and control model, but that's not very interesting to ASP.NET MVC developers. With ASP.NET MVC, you're free to use any Ajax or JavaScript library.

The most popular option, judging by the overwhelming roar of approval coming from the world's web developers, is to use jQuery. This option has become so popular that Microsoft now ships jQuery with ASP.NET MVC, even though it isn't a Microsoft product. So, what's all the fuss about?

Using jQuery with ASP.NET MVC

Write less, do more: that's the core promise of jQuery, a free, open source[87] JavaScript library first released in 2006. It's won massive kudos from web developers on all platforms because it cuts the pain out of client-side coding. It provides an elegant CSS 3-based syntax for traversing your DOM, a fluent API for manipulating and animating DOM elements, and extremely concise wrappers for Ajax calls—all carefully abstracted to eliminate cross-browser differences.[88] It's easily extensible, has a rich ecosystem of free plug-ins, and encourages a coding style that retains basic functionality when JavaScript isn't available.

Sounds too good to be true? Well, I can't really claim that it makes all client-side coding easy, but it is usually far easier than raw JavaScript, and it works great with ASP.NET MVC. Over the next few pages, you'll learn the basic theory of jQuery and see it in action, adding some sparkle to typical ASP.NET MVC actions and views.

Referencing jQuery

Every new ASP.NET MVC project already has jQuery in its /Scripts folder. Like many other JavaScript libraries, it's just a single .js file. To use it, you only need to reference it.

For example, in your application's master page, add the following <script> tag at the top of the <head> section:

<head runat="server">
    <script src="<%: Url.Content("~/Scripts/jquery-1.4.1.min.js") %>"
            type="text/javascript"></script>
    <!-- Leave rest as before -->
</head>

Note

Earlier in this chapter, I recommended that for best performance, you should reference external JavaScript files by placing <script> tags near the bottom of your HTML document. You could reference jQuery like that, but if you do, you must then be sure to put any JavaScript blocks that call jQuery even later in the HTML document (scripts are loaded and executed in document order, and you can't call a script until it's been loaded). To simplify this chapter I'm recommending that you load jQuery from your page's <head> section so that you can call jQuery as part of the page loading process. You can reposition your <script> tags later if you're keen to optimize your page load times.

jquery-1.4.1.min.js is the minified version, which means that comments, long variable names, and unnecessary whitespace have been stripped out to reduce download times. If you want to read and understand jQuery's source code, read the nonminified version (jquery-1.4.1.js) instead.

If you like, you can get the latest version of jQuery from http://jquery.com/. Download the core jQuery library file, put it in your application's /Scripts folder, and then reference it as just shown. At the time of writing, the latest version is 1.4.2.

Referencing jQuery on a Content Delivery Network

jQuery is now so outrageously popular (at the time of writing, BuiltWith estimates that nearly 30 percent of all web sites use it[89]) that it seems wasteful for every web site to maintain and serve its own separate copy. If instead we all referenced a single central copy, there'd be benefits all round:

  • You would no longer need to pay for the bandwidth involved in shipping jQuery to every visitor.

  • Visitors would get faster page loads, because their browser wouldn't need to download the jQuery library yet again (normally, it would already be stored in their cache).

To bring this idea closer to reality, Google and Microsoft have both placed various versions of jQuery and related JavaScript libraries on their worldwide content delivery networks (CDNs), and have invited you to reference them directly. Their CDN systems attempt to direct incoming traffic to geographically local servers, so users get fast responses wherever they are in the world.

Microsoft's copies of jQuery are stored under ajax.microsoft.com/ajax/jquery/, so to reference jQuery 1.4.1, you can use the following tag:

<script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.1.min.js"
        type="text/javascript"></script>

Microsoft also hosts other JavaScript libraries, including jQuery Validation (and of course Microsoft's own ASP.NET AJAX libraries). For details, see www.asp.net/ajaxLibrary/cdn.ashx.

Google's approach is similar. You can reference their copies of jQuery under ajax.googleapis.com/ajax/libs/jquery/—for example:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js"
        type="text/javascript"></script>

Warning

One possible drawback to using a third-party CDN is that you're implicitly trusting the host not to include any malicious code in the scripts they serve, and you're giving the host a way of passively measuring the traffic on your web site. Most companies will not seriously worry about Microsoft or Google planting malicious code into their copy of jQuery, but if you're directly in competition against either of them, you should at least consider whether exposing your traffic statistics is acceptable.

Basic jQuery Theory

At the heart of jQuery is a powerful JavaScript function called jQuery(). You can use it to query your HTML page's DOM for all elements that match a CSS selector. For example, jQuery("DIV.MyClass") finds all the divs in your document that have the CSS class MyClass.

jQuery() returns a jQuery-wrapped set: an instance of a jQuery object that lists the results and has many extra methods you can call to operate on those results. Most of the jQuery API consists of such methods on wrapped sets. For example, jQuery("DIV.MyClass").hide() makes all the matching divs suddenly vanish.

For brevity, jQuery provides a shorthand syntax, $(), which is exactly the same as calling jQuery().[90] Table 14-3 gives some more examples of its use.

Table 14-3. Simple jQuery Examples

Code

Effect

$("P SPAN").addClass("SuperBig")

Adds a CSS class called SuperBig to all <span> nodes that are contained inside a <p> node

$(".SuperBig").removeClass("SuperBig")

Removes the CSS class called SuperBig from all nodes that have it

$("#options").toggle()

Toggles the visibility of the element with ID options (if it's visible, it will be hidden; if it's already hidden, it will be shown)

$("DIV:has(INPUT[type='checkbox']:disabled)").prepend("<i>Hey!</i>")

Inserts the HTML markup <i>Hey!</i> at the top of all divs that contain a disabled check box

$("#options A").css("color", "red").fadeOut()

Finds any hyperlink tags (i.e., <a> tags) contained within the element with ID options, sets their text color to red, and fades them out of view by slowly adjusting their opacity to zero

As you can see, this is extremely concise. Writing the same code without jQuery would take many lines of JavaScript. The last two examples demonstrate two of jQuery's important features:

  • CSS 3 support: When supplying selectors to jQuery, you can use the vast majority of CSS 3-compliant syntax, regardless of whether the underlying browser itself supports it. This includes pseudoclasses such as :has(child selector), :first-child, :nth-child, and :not(selector), along with attribute selectors such as *[att='val'] (matches nodes with attribute att="val"), sibling combinators such as table + p (matches paragraphs immediately following a table), and child combinators such as body > div (matches divs that are immediate children of the <body> node).

  • Method chaining: Almost all methods that acton wrapped sets also return wrapped sets, so you can chain together a series of method calls (e.g., $(selector).abc().def().ghi()—permitting very succinct code).

Over the next few pages, you'll learn about jQuery as a stand-alone library. After that, I'll demonstrate how you can use many of its features in an ASP.NET MVC application.

Note

This isn't intended to be a complete reference to jQuery, because it's separate from ASP.NET MVC. I will simply demonstrate jQuery working with ASP.NET MVC without documenting all the jQuery method calls and their many options—you can easily look them up online (see http://docs.jquery.com/ or http://visualjquery.com/). For a full guide to jQuery, I recommend jQuery in Action, by Bear Bibeault and Yehuda Katz (Manning, 2008).

Waiting for the DOM

Most browsers will run JavaScript code as soon as the page parser hits it, before the browser has even finished loading the page. This presents a difficulty, because if you place some JavaScript code at the top of your HTML page, inside its <head> section, then the code won't immediately be able to operate on the rest of the HTML document—the rest of the document hasn't even loaded yet.

Traditionally, web developers have solved this problem by invoking their initialization code from an onload handler attached to the <body> element. This ensures the code runs only after the full document has loaded. There are two drawbacks to this approach:

  • The <body> tag can have only one onload attribute, so it's awkward if you're trying to combine multiple independent pieces of code.

  • The onload handler waits not just for the DOM to be loaded, but also for all external media (such as images) to finish downloading. Your rich user experience doesn't get started as quickly as you might expect, especially on slow connections.

The perfect solution is to tell the browser to run your startup code as soon as the DOM is ready, but without waiting for external media. The API varies from one browser to the next, but jQuery offers a simple abstraction that works on them all. Here's how it looks:

<script>
    $(function() {
        // Insert your initialization code here
    });
</script>

By passing a JavaScript function to $(), such as the anonymous function in the preceding code, you register it for execution as soon as the DOM is ready. You can register as many such functions as you like. For example, you could have a whole range of independent behaviors described in separate external .js files, each of which uses one of these DOM-ready handlers to initialize itself as soon as the page is loaded.

Event Handling

Ever since Netscape Navigator 2 (1996), it's been possible to hook up JavaScript code to handle client-side UI events (such as click, keydown, and focus). For the first few years, the events API was totally inconsistent from one browser to another—not only the syntax to register an event, but also the event-bubbling mechanisms and the names for commonly used event properties (do you want pageX, screenX, or clientX?). Internet Explorer was famous for its pathological determination to be the odd one out every time.

Since those dark early days, modern browsers have become . . . no better at all! We're still in this mess more than a decade later, and even though the W3C has ratified a standard events API (see www.w3.org/TR/DOM-Level-2-Events/events.html), few browsers support much of it. And in today's world, where Firefox, iPhones, Nintendo Wiis, and small cheap laptops running Linux are all commonplace, your application needs to support an unprecedented diversity of browsers and platforms.

jQuery makes a serious effort to attack this problem. It provides an abstraction layer above the browser's native JavaScript API, so your code will work just the same on any jQuery-supported browser. Its syntax for handling events is pretty slick—for example:

$("img").click(function() { $(this).fadeOut() })

causes each image to fade out when you click it. (Obviously, you have to put this inside <script></script> tags to make it work.)

Note

Wondering what $(this) means? In the event handler; JavaScript's this variable references the DOM element receiving the event. However, that's just a plain old DOM element, so it doesn't have a fadeOut() method. That's why you need to write $(this), which creates a wrapped set (containing just one element, this) endowed with all the capabilities of a jQuery-wrapped set (including the jQuery method fadeOut()).

Notice that it's no longer necessary to worry about the difference between addEventListener() for standards-compliant browsers and attachEvent() for Internet Explorer 6, and we're way beyond the nastiness of putting event handler code right into the element definition (e.g., <img src="..." onclick="some JavaScript code"/>), which doesn't support multiple event handlers. You'll see more jQuery event handling in the upcoming examples.

Global Helpers

Besides methods that operate on jQuery-wrapped sets, jQuery offers a number of global properties and functions designed to simplify Ajax and work around cross-browser scripting and box model differences. You'll learn about jQuery Ajax later. Table 14-4 gives some examples of jQuery's other helpers.

Table 14-4. A Few Global Helper Functions Provided by jQuery

Method

Description

$.browser

Tells you which browser is running, according to the user-agent string. You'll find that one of the following is set to true:$.browser.msie, $.browser.mozilla, $.browser.safari, or $.browser.opera.

$.browser.version

Tells you which version of that browser is running.

$.support

Detects whether the browser supports various facilities. For example, $.support.boxModel determines whether the current frame is being rendered according to the W3C standard box model.[a] Check the jQuery documentation for a full list of what capabilities $.support can detect.

$.trim(str)

Returns the string str with leading and trailing whitespace removed. jQuery provides this useful function because, strangely, there's no such function in regular JavaScript.

$.inArray(val, arr)

Returns the first index of val in the array arr. jQuery provides this useful function because Internet Explorer, even in version 8, doesn't otherwise have an array.indexOf() function.

[a] The box model specifies how the browser lays out elements and computes their dimensions, and how padding and border styles are factored into the decision. This can vary according to browser version and which DOCTYPE your HTML page declares. Sometimes you can use this information to fix layout differences between browsers by making slight tweaks to padding and other CSS styles.

This isn't the full set of helper functions and properties in jQuery, but the full set is actually quite small. jQuery's core is designed to be extremely tight for a speedy download, while also being easily extensible so you can write a plug-in to add your own helpers or functions that operate on wrapped sets.

Unobtrusive JavaScript

You're almost ready to start using jQuery with ASP.NET MVC, but there's just one more bit of theory you need to get used to: unobtrusive JavaScript.

What's that then? It's the principle of keeping your JavaScript code clearly and physically separate from the HTML markup on which it operates, aiming to keep the HTML portion still functional in its own right. For example, don't write this:

<div id="mylinks">
    <a href="#" onclick="if(confirm('Follow the link?'))
                            location.href = '/someUrl1';">Link 1</a>
    <a href="#" onclick="if(confirm('Follow the link?'))
                            location.href = '/someUrl2';">Link 2</a>
</div>

Instead, write this:

<div id="mylinks">
    <a href="/someUrl1">Link 1</a>
    <a href="/someUrl2">Link 2</a>
</div>

<script type="text/javascript">
    $("#mylinks a").click(function() {
        return confirm("Follow the link?");
    });
</script>

This latter code is better not just because it's easier to read, and not just because it doesn't involve repeating code fragments. The key benefit is that it's still functional even for browsers that don't support JavaScript. The links can still behave as ordinary links.

There's a design process you can adopt to make sure your JavaScript stays unobtrusive:

  • First, build the application or feature without using any JavaScript at all, accepting the limitations of plain old HTML/CSS, and getting viable (though basic) functionality.

  • After that, you're free to layer on as much rich cross-browser JavaScript as you like—Ajax, animations . . . go wild!—just don't touch the original markup. Preferably, keep your script in a separate file, so as to remind yourself that it's distinct. You can radically enhance the application's functionality without affecting its behavior when JavaScript is disabled.

Because unobtrusive JavaScript doesn't need to be injected at lots of different places in the HTML document, your MVC views can be simpler, too. You certainly won't find yourself constructing JavaScript code using server-side string manipulation in a <% foreach(...) %> loop!

jQuery makes it relatively easy to add an unobtrusive layer of JavaScript, because after you've built clean, scriptless markup, it's usually just a matter of a few jQuery calls to attach sophisticated behaviors or eye candy to a whole set of elements. Let's see some real-world examples.

Adding Client-Side Interactivity to an MVC View

Everyone loves a grid. Imagine you have a model class called MountainInfo, defined as follows:

public class MountainInfo
{
    public string Name { get; set; }
    public int HeightInMeters { get; set; }
}

You could render a collection of MountainInfo objects as a grid, using a strongly typed view whose model type is IEnumerable<MountainInfo>, containing the following markup:

<h2>The Seven Summits</h2>
<div id="summits">
    <table>
        <thead><tr>
            <td>Item</td><td>Height (m)</td><td>Actions</td>
        </tr></thead>
        <% foreach(var mountain in Model) { %>
<tr>
                <td><%: mountain.Name %></td>
                <td><%: mountain.HeightInMeters %></td>
                <td>
                    <% using(Html.BeginForm("DeleteItem", "Home")) { %>
                        <%: Html.Hidden("item", mountain.Name) %>
                        <input type="submit" value="Delete" />
                    <% } %>
                </td>
            </tr>
        <% } %>
    </table>
</div>

It's not very exciting, but it works, and there's no JavaScript involved. With some appropriate CSS and a suitable DeleteItem() action method, this will display and behave as shown in Figure 14-6.

A basic grid that uses no JavaScript

Figure 14-6. A basic grid that uses no JavaScript

To implement the Delete buttons, it's the usual "multiple forms" trick: each Delete button is contained in its own separate <form>, so it can invoke an HTTP POST—without JavaScript—to a different URL according to which item is being deleted. (We'll ignore the more difficult question of what it means to delete a mountain.)

Now let's improve the user experience in three ways using jQuery. None of the following changes will affect the application's behavior if JavaScript isn't enabled.

Improvement 1: Zebra-Striping

This is a common web design convention: you style alternating rows of a table differently, creating horizontal bands that help the visitor to parse your grid visually. ASP.NET Web Forms' DataGrid and GridView controls have built-in means to achieve it. In ASP.NET MVC, you could achieve it by applying a special CSS class to every second <TR> tag, as follows:

<% int i = 0; %>
<% foreach(var mountain in Model) { %>
<tr <%= i++ % 2 == 1 ? "class='alternate'" : "" %>>

but I think you'll agree that code is pretty unpleasant. You could use a CSS 3 pseudoclass:

tr:nth-child(even) { background: silver; }

but you'll find that relatively few browsers support it natively. So, bring in one line of jQuery. You can add the following anywhere in a view, such as in the <head> section of a master page (as long as it's after the <script> tag that references jQuery itself), or into a view near the markup upon which it acts:

<script type="text/javascript">
    $(function() {
        $("#summits tr:nth-child(even)").css("background-color", "silver");
    });
</script>

That works on any mainstream browser, and produces the display shown in Figure 14-7. Notice how we use $(function() { ... }); to register the initialization code to run as soon as the DOM is ready.

Note

Throughout the rest of this chapter, I won't keep reminding you to register your initialization code using $(function() { ... });. You should take it for granted that whenever you see jQuery code that needs to run on DOM initialization, you should put it inside a $(function() { ... }); block (also known as a DOM-ready handler).

The zebra-striped grid

Figure 14-7. The zebra-striped grid

To make this code tidier, you could use jQuery's shorthand pseudoclass :even, and apply a CSS class:

$("#summits tr:even").addClass("alternate");

Improvement 2: Confirm Before Deletion

It's generally expected that you'll give people a warning before you perform a significant, irrevocable action, such as deleting an item.[91] Don't render fragments of JavaScript code into onclick="..." or onsubmit="..." attributes—assign all the event handlers at once using jQuery. Add the following to a jQuery DOM-ready handler:

$("#summits form[action$='/DeleteItem']").submit(function() {
    var itemText = $("input[name='item']", this).val();
    return confirm("Are you sure you want to delete '" + itemText + "'?");
});

This query scans the summits element, finding all <form> nodes that post to a URL ending with the string /DeleteItem, and intercepts their submit events. The behavior is shown in Figure 14-8.

The submit event handler firing

Figure 14-8. The submit event handler firing

Improvement 3: Hiding and Showing Sections of the Page

Another common usability trick is to hide certain sections of the page until you know for sure that they're currently relevant to the user. For example, on an e-commerce site, there's no point showing input controls for credit card details until the user has selected the "pay by credit card" option. As mentioned in the previous chapter, this is called progressive disclosure.

For another example, you might decide that certain columns on a grid are optional—hidden or shown according to a check box. That would be quite painful to achieve normally: if you did it on the server (a laASP.NET Web Forms), you'd have tedious round trips, state management, and messy code to render the table; if you did it on the client, you'd have to fuss about event handling and cross-browser CSS differences (e.g., displaying cells using display:table-cell for standards-compliant browsers, and display:block for Internet Explorer 7).

But you can forget all those problems. jQuery makes it quite simple. Add the following initialization code:

$("<label><input id='heights' type='checkbox'/>Show heights</label>")
    .insertBefore("#summits")
    .children("input").click(function () {
        $("#summits td:nth-child(2)").toggle(this.checked);
    });
$("#summits td:nth-child(2)").hide();

That's all you need. By passing an HTML string to $(), you instruct jQuery to create a set of DOM elements matching your markup. The code dynamically inserts this new check box element immediately before the summits element, and then binds a click event handler that toggles the visibility of the grid's second column according to the check box state.

The final line of code causes the second column to be initially hidden. Any cross-browser differences are handled transparently by jQuery's abstraction layer. The new behavior is shown in Figure 14-9.

Hide and show a column by clicking a check box.

Figure 14-9. Hide and show a column by clicking a check box.

Notice that this really is unobtrusive JavaScript. First, it doesn't involve any changes to the server-generated markup for the table, and second, it doesn't interfere with appearance or behavior if JavaScript is disabled. The "Show heights" check box isn't even added unless JavaScript is supported.

Ajax-Enabling Links and Forms

Now let's get on to the real stuff. You've already seen how to use ASP.NET MVC's built-in Ajax helpers to perform partial page updates without writing any JavaScript. You also learned that there are a number of limitations with this approach.

You could overcome those limitations by writing raw JavaScript, but you'd encounter problems such as the following:

  • The XMLHttpRequest API, the core mechanism used to issue asynchronous requests, follows the beloved browser tradition of requiring different syntaxes depending on browser type and version. Internet Explorer 6 requires you to instantiate an XMLHttpRequest object using a nonstandard syntax based around ActiveX. Other browsers have a cleaner, different syntax.

  • It's a pretty clumsy and verbose API, requiring you to do obscure things such as track and interpret readyState values.

As usual, jQuery brings simplicity. For example, the complete code needed to load content asynchronously into a DOM element is merely this:

$("#myElement").load("/some/url");

This constructs an XMLHttpRequest object (in a cross-browser fashion), sets up a request, waits for the response, and if the response is successful, copies the response markup into each element in the wrapped set (i.e., myElement). Easy!

Unobtrusive JavaScript and Hijaxing

So, how does Ajax fit into the world of unobtrusive JavaScript? Naturally, your Ajax code should be separated clearly from the HTML markup it works with. Also, if possible, you'll design your application to work acceptably even when JavaScript isn't enabled. First, create links and forms that work one way without JavaScript. Next, write script that intercepts and modifies their behavior when JavaScript is available.

This business of intercepting and changing behavior is known as hijacking. Some people even call it hijaxing, since the usual goal is to add Ajax functionality. Unlike most forms of hijacking, this one is a good thing.

Hijaxing Links

Let's go back to the grid example from earlier and add paging behavior. First, design the behavior to work without any JavaScript at all. That's quite easy—you can reuse some of the paging code from the SportsStore example. See the instructions in the "Displaying Page Links" section in Chapter 4 to create the classes PagingInfo and PagingHelpers, and reference both of their namespaces in Web.config as described in the "Making the HTML Helper Method Visible to All View Pages" section (also in Chapter 4).

Next, add an optional page parameter to the Summits() action method, and pick out the requested page of data:

private const int PageSize = 3;
public ActionResult Summits([DefaultValue(1)] int page)
{
    var allItems = SampleData.SevenSummits;

    ViewData["pagingInfo"] = new PagingInfo {
        CurrentPage = page,
        ItemsPerPage = PageSize,
        TotalItems = allItems.Count
    };

    return View(allItems.Skip((page − 1)*PageSize).Take(PageSize));
}

Note

If you prefer not to use ViewData as a dictionary like this, you can follow the approach used for SportsStore and create an additional view model class with two properties to hold all the data for the view. It will need one property of type PagingInfo and another property of type IEnumerable<MountainInfo>.

Now you can update the view to render page links. You've already got the Html.PageLinks() helper in place, so update your view as follows:

<h2>The Seven Summits</h2>
<div id="summits">
    <table>
        <!-- ... exactly as before ... -->
    </table>
    Page:
    <%: Html.PageLinks((PagingInfo)ViewData["pagingInfo"],
                       i => Url.Action("Summits", new { page = i })) %>
</div>
<p><i>This page generated at <%: DateTime.Now.ToLongTimeString() %></i></p>

I've added the timestamp just to make it clear when Ajax is (and is not) working. Here's how it looks in a browser with JavaScript disabled (Figure 14-10).

Simple server-side paging behavior (with JavaScript disabled in the browser)

Figure 14-10. Simple server-side paging behavior (with JavaScript disabled in the browser)

The timestamps are all slightly different, because each of these three pages was generated at a different time. Notice also that the zebra striping is gone, along with the other jQuery-powered enhancements (obviously—JavaScript is disabled!). However, the basic behavior still works.

Performing Partial Page Updates

Now that the scriptless implementation is in place, it's time to layer on some Ajax magic. We'll allow the visitor to move between grid pages without a complete page update. Each time they click a page link, we'll fetch and display the requested page asynchronously.

To do a partial page update with jQuery, you can intercept a link's click event, fetch its target URL asynchronously using the $.get() helper, extract the portion of the response that you want, and then paste it into the document using .replaceWith(). It may sound complicated, but the code needed to apply it to all links matching a selector isn't so bad:

$("#summits a").click(function() {
    $.get($(this).attr("href"), function(response) {
        $("#summits").replaceWith($("#summits", response));
    });
    return false;
});

Notice that the click handler returns false, preventing the browser from doing traditional navigation to the link's target URL. Also beware that there is a quirk in jQuery 1.4.1 that you might need to work around,[92] depending on how you've structured your HTML document.Figure 14-11 shows the result.

First attempt at Ajax paging with jQuery. Spot the bugs.

Figure 14-11. First attempt at Ajax paging with jQuery. Spot the bugs.

Hmm, there's something strange going on here. The first click was retrieved asynchronously (see, the timestamp didn't change), although we lost the zebra striping for some reason. By the second click, the page wasn't even fetched asynchronously (the timestamp did change). Huh?

Actually, it makes perfect sense: the zebra striping (and other jQuery-powered behavior) only gets added when the page first loads, so it isn't applied to any new elements fetched asynchronously. Similarly, the page links are only hijaxed when the page first loads, so the second set of page links has no Ajax powers. The magic has faded away!

Fortunately, it's quite easy to register the JavaScript-powered behaviors in a slightly different way so that they stay effective even as the DOM keeps changing.

Using live to Retain Behaviors After Partial Page Updates

jQuery's live() method lets you register event handlers so that they apply not just to matching elements in the initial DOM, but also to matching elements introduced when the DOM is updated later. This lets us solve the problem we encountered a moment ago.

For example, to ensure that the deletion confirmation behavior applies to all Delete buttons, no matter whether they're in the initial DOM or are added later, change the way you bind to their submit events by using live() as follows:

$("#summits form[action$='/DeleteItem']").live("submit", function () {
    var itemText = $("input[name='item']", this).val();
    return confirm("Are you sure you want to delete '" + itemText + "'?");
});

Next, to avoid losing the page links hijaxing behavior whenever the DOM is rebuilt, change how you bind to the links' click events by using live() as follows:

$("#summits a").live("click", function () {
    $.get($(this).attr("href"), function (response) {
        $("#summits").replaceWith($("#summits", response));

        // Reapply zebra striping
        $("#summits tr:even").addClass("alternate");

        // Respect the (un)checked state of the "show heights" check box
$("#summits td:nth-child(2)").toggle($("#heights")[0].checked);
    });
    return false;
});

This takes care of preserving all behaviors, including the hijaxed behavior of the links, and whether or not to show the Heights column, however much the visitor switches between pages. It behaves as shown in Figure 14-12.

Ajax paging is now working properly.

Figure 14-12. Ajax paging is now working properly.

Tip

If you use jQuery's live() method often, then take a look at the liveQuery plug-in (http://plugins.jquery.com/project/livequery), which makes the method more powerful. With this plug-in, the preceding code can be made simpler: you can eliminate the initializeTable() method and simply declare that all the behaviors should be retained no matter how the DOM changes.

Hijaxing Forms

Sometimes, you don't just want to hijack a link—you want to hijack an entire <form> submission. You've already seen how to do this with ASP.NET MVC's Ajax.BeginForm() helper. For example, it means you can set up a <form> asking for a set of search parameters, and then submit it and display the results without a full-page refresh. Naturally, if JavaScript were disabled, the user would still get the results, but via a traditional full-page refresh. Or, you might use a <form> to request specific non-HTML data from the server, such as current product prices in JSON format, without causing a full-page refresh.

Here's a very simple example. Let's say you want to add a stock quote lookup box to one of your pages. You might have an action method called GetQuote() on a controller called StocksController:

public class StocksController : Controller
{
    public string GetQuote(string symbol)
    {
        // Obviously, you could do something more intelligent here
        if (symbol == "GOOG")
            return "$9999";
        else
            return "Sorry, unknown symbol";
    }
}

and elsewhere, some portion of a view like this:

<h2>Stocks</h2>
<% using(Html.BeginForm("GetQuote", "Stocks")) { %>
    Symbol:
    <%: Html.TextBox("symbol") %>
    <input type="submit" />
    <span id="results"></span>
<% } %>
<p><i>This page generated at <%: DateTime.Now.ToLongTimeString() %></i></p>

Now you can Ajax-enable this form easily, as follows (remember to reference jQuery and register this code to run when the DOM is loaded):

$("form[action$='GetQuote']").submit(function() {
    $.post($(this).attr("action"), $(this).serialize(), function(response) {
        $("#results").html(response);
    });
    return false;
});

This code finds any <form> that would be posted to a URL ending with the string GetQuote and intercepts its submit event. The handler performs an asynchronous POST to the form's original action URL, sending the form data as usual (formatted for an HTTP request using $(this).serialize()), and puts the result into the <span> element with ID results. As usual, the event handler returns false so that the <form> doesn't get submitted in the traditional way. Altogether, it produces the behavior shown in Figure 14-13.

A trivial hijaxed form inserting its result into the DOM

Figure 14-13. A trivial hijaxed form inserting its result into the DOM

Note

This example doesn't provide any sensible behavior for non-JavaScript-supporting clients. For those, the whole page gets replaced with the stock quote. To support non-JavaScript clients, you could alter GetQuote() to render a complete HTML page if Request.IsAjaxRequest() returns false.

Client/Server Data Transfer with JSON

Frequently, you might need to transfer more than a single data point back to the browser. What if you want to send an entire object, an array of objects, or a whole object graph? The JSON data format (see www.json.org/) is ideal for this: it's more compact than preformatted HTML or XML, and it's natively understood by any JavaScript-supporting browser. ASP.NET MVC has special support for sending JSON data, and jQuery has special support for receiving it. From an action method, return a JsonResult object by calling Json(), passing a .NET object for it to convert—for example:

public class StockData
{
    public decimal OpeningPrice { get; set; }
    public decimal ClosingPrice { get; set; }
public string Rating { get; set; }
}

public class StocksController : Controller
{
    public JsonResult GetQuote(string symbol)
    {
        // You could fetch some real data here
        if(symbol == "GOOG")
            return Json(new StockData {
                OpeningPrice = 556.94M, ClosingPrice = 558.20M, Rating = "A+"
            });
        else
            return null;
    }
}

In case you haven't seen JSON data before, this action method sends the following string:

{"OpeningPrice":556.94,"ClosingPrice":558.2,"Rating":"A+"}

This is JavaScript's native "object notation" format—it actually is JavaScript source code.[93] ASP.NET MVC constructs this string using .NET's System.Web.Script.Serialization.JavaScriptSerializer API, passing along your StockData object. JavaScriptSerializer uses reflection to identify the object's properties, and then renders it as JSON.

Note

Although .NET objects can contain both data and code (i.e., methods), their JSON representation only includes the data portion—methods are skipped. There's no (simple) way of translating .NET code to JavaScript code.

On the client, you can fetch the JSON data using jQuery's all-purpose $.ajax() method. First update the view as follows:

<h2>Stocks</h2>
<% using(Html.BeginForm("GetQuote", "Stocks")) { %>
    Symbol:
    <%: Html.TextBox("symbol") %>
    <input type="submit" />
<% } %>
<table>
    <tr><td>Opening price:</td><td><div id="openingPrice" /></td></tr>
    <tr><td>Closing price:</td><td><div id="closingPrice" /></td></tr>
    <tr><td>Rating:</td><td><div id="stockRating" /></td></tr>
</table>

<p><i>This page generated at <%: DateTime.Now.ToLongTimeString() %></i></p>

Then change the hijaxing code so that it fetches the JSON object using $.ajax() and then displays each resulting StockData property in the corresponding table cell:

$("form[action$='GetQuote']").submit(function () {
    $.ajax({
        url: $(this).attr("action"),
        type: "post",
        data: $(this).serialize(),
        success: function(stockData) {
            $("#openingPrice").html(stockData.OpeningPrice);
            $("#closingPrice").html(stockData.ClosingPrice);
            $("#stockRating").html(stockData.Rating);
        }
    });
    return false;
});

This produces the behavior shown in Figure 14-14.

Fetching and displaying a JSON data structure

Figure 14-14. Fetching and displaying a JSON data structure

If you make extensive use of JSON in your application, you could start to think of the server as being just a collection of JSON web services,[94] with the browser taking care of the entire UI. That's a valid architecture for a very modern web application (assuming you don't also need to support non-JavaScript clients). You'd benefit from all the power and directness of the ASP.NET MVC Framework but would skip over the view engine entirely.

A Note About JsonResult and GET Requests

In the preceding example, I passed the option type: "post" to $.ajax() so that it would fetch the JSON object via a POST request. Without this, jQuery would have tried to use a GET request by default, and it would have failed.

It wouldn't have been obvious what was wrong—the stock quote information would have silently failed to appear—but if you used a debugging aid such as FireBug (a Firefox add-on), you would have seen that ASP.NET MVC responded with "500 Server Error." To see the error first-hand, you can try to fetch the JSON object through a GET request by navigating to /Stocks/GetQuote?symbol=GOOG, as shown in Figure 14-15.

By default, JsonResult refuses to serve GET requests.

Figure 14-15. By default, JsonResult refuses to serve GET requests.

This is all about a security issue that applies when you serve JSON data over GET requests. Not all browsers can be trusted to protect the data from being leaked to third-party domains (for details, see http://haacked.com/archive/2009/06/25/json-hijacking.aspx). To mitigate this risk, Microsoft changed the behavior of JsonResult in ASP.NET MVC 2 so that it won't allow GET requests by default.

If you want to allow GET requests to fetch your JSON data, update the action method's return statement as follows:

return Json(new StockData {
    OpeningPrice = 556.94M,
    ClosingPrice = 558.20M,
    Rating = "A+"
}, JsonRequestBehavior.AllowGet);

Of course, you're then accepting the risk that, depending on what browser a user is using, the data could be leaked to a third-party domain. Typically you don't need to take this risk—just do your JSON-fetching Ajax calls using POST requests, as shown in the earlier example.

Performing Cross-Domain JSON Requests Using JSONP

Normally, the browser's security model restricts your pages to making Ajax requests only to URLs on the same domain. Without this protection, a malicious script could, for example, use Ajax to request data from a victim's web mail or online bank account (since the victim's browser has probably already authenticated itself to the web mail or bank web site) and then post the private data to some other server under the attacker's control.

But what if you really need to fetch JSON data from a different domain? A common scenario is needing to perform requests from http://yoursite to https://yoursite or vice versa. Well, as long as you are in control of both domains, there are at least two ways to work around the restriction:

  • You can use the Cross Origin Resource Sharing protocol as described at www.w3.org/TR/cors/. The idea with this protocol is that, when your server responds to a request, it may set special HTTP headers such as Access-Control-Allow-Origin that instruct the browser to bypass the usual same-domain restrictions—either granting access to requests from all origins, or to requests from a specific set of domains. Unfortunately, this protocol is supported only by relatively modern browsers (e.g., Firefox 3.5, Internet Explorer 8, and Safari 4), so it's currently suitable only for intranet applications where you can dictate which browsers may be used.

  • You can use JSONP, a way of retrieving JSON data using <script> tags that, for historical reasons, are allowed to work across domains. It works as follows:

    1. The host page sets up a temporary callback function with some random unique name (e.g., callback28372()).

    2. The host page creates a <script> tag referencing the desired data's URL with the callback function name appended as a query string parameter (e.g., <script src="http://example.com/url?callback=callback28372"></script>).

    3. This <script> tag causes the browser to perform a GET request to the specified URL and evaluate the result as a JavaScript block. Because <script> tags have been allowed to do this since the dawn of the Web, long before modern browser security restrictions, this is allowed regardless of whether the request crosses a domain boundary. Note that <script> tags can only cause GET requests, so JSONP cannot perform POST requests.

    4. The target server receives this GET request and returns some JSON data object wrapped in a JavaScript method call (e.g., callback28372({ data: "value", ... })).

    5. The browser runs the <script> block, which means the temporary callback function receives the JSON data object.

You may be thinking that JSONP is really just a hack, and if so, you're right. However it works reliably on virtually all browsers, so it's been formalized as a native feature in jQuery. You don't have to carry out steps 1 through 3 or step 5, because jQuery will do it for you. All you have to implement is step 4, which happens on the server.

Continuing the previous example, your form might reference some URL on a different domain, as follows:

<form action="http://some-other-domain/Stocks/GetQuoteJsonP">
    Symbol:
    <%: Html.TextBox("symbol") %>
    <input type="submit" />
</form>

To tell jQuery to use the JSONP protocol to retrieve this data, you just need to add a dataType parameter. Update your hijaxing code as follows:

$("form[action$='GetQuoteJsonP']").submit(function () {
    $.ajax({
        url: $(this).attr("action"),
        data: $(this).serialize(),
        dataType: "jsonp",
        success: function (stockData) {
            $("#openingPrice").html(stockData.OpeningPrice);
            $("#closingPrice").html(stockData.ClosingPrice);
            $("#stockRating").html(stockData.Rating);
        }
    });
    return false;
});

jQuery will now automatically use a <script> block to perform a GET request to the target URL (appending a query string parameter called callback). But this won't work until your server cooperates by returning an instruction to invoke the callback method. A neat way to do this is to wrap the behavior in a custom action result, JsonpResult, so your action method hardly needs to change:

public JsonpResultGetQuoteJsonP(string symbol)
{
    // You could fetch some real data here
    if (symbol == "GOOG")
        return new JsonpResult(new StockData
        {
            OpeningPrice = 556.94M,
            ClosingPrice = 558.20M,
            Rating = "A+"
        });
    else
        return null;
}

You can implement JsonpResult as follows, placing this class anywhere in your ASP.NET MVC application:

public class JsonpResult : ActionResult
{
    private object Data { get; set; }
    public JsonpResult(object data) {
        Data = data;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        context.HttpContext.Response.Write(string.Format("{0}({1});",
            context.HttpContext.Request["callback"],   // Callback method name
            new JavaScriptSerializer().Serialize(Data) // Data formatted as JSON
        ));
    }
}

This action result performs step 4 in the preceding description of JSONP, so this completes the task and enables cross-domain access.

Warning

Once you start using JSONP, you're deliberately bypassing the browser's usual same-domain security policy, so it becomes easy for scripts on any third-party domain to read the data. This could violate your users' privacy. Be careful what data you expose through a JsonpResult.

Fetching XML Data Using jQuery

If you prefer, you can use XML format instead of JSON format in all these examples. jQuery will deal with the client-side XML parsing for you.

First, you need to return XML from an action method. For example, update the previous GetQuote() method as follows, using a ContentResult to set the correct content-type header:

public ContentResult GetQuote(string symbol)
{
    // Return some XML data as a string
    if (symbol == "GOOG") {
        return Content(
            new XDocument(new XElement("Quote",
                 new XElement("OpeningPrice", 556.94M),
                 new XElement("ClosingPrice", 558.20M),
                 new XElement("Rating", "A+")
            )).ToString()
        , System.Net.Mime.MediaTypeNames.Text.Xml);
    }
    else
        return null;
}

Given the parameter GOOG, this action method will produce the following output:

<Quote>
  <OpeningPrice>556.94</OpeningPrice>
  <ClosingPrice>558.20</ClosingPrice>
  <Rating>A+</Rating>
</Quote>

Next, tell jQuery that when it gets the response, it should interpret it as XML rather than as plain text or JSON. Parsing the response as XML gives you the convenience of using jQuery itself to extract data from the resulting XML document. For example, update the preceding form submit handler as follows:

$("form[action$='GetQuote']").submit(function() {
    $.ajax({
        url: $(this).attr("action"),
        data: $(this).serialize(),
        dataType: "xml", // Instruction to parse response as XMLDocument
        success: function(resultXml) {
            // Extract data from XMLDocument using jQuery selectors
            var opening = $("OpeningPrice", resultXml).text();
            var closing = $("ClosingPrice", resultXml).text();
            var rating = $("Rating", resultXml).text();
            // Use that data to update DOM
            $("#openingPrice").html(opening);
            $("#closingPrice").html(closing);
            $("#stockRating").html(rating);
        }
    });
    return false;
});

The application now has exactly the same behavior as it did when sending JSON, as depicted in Figure 14-14, except that the data is transmitted as XML. This works fine, but most web developers still prefer JSON because it's more compact and readable. Also, working with JSON means that you don't have to write so much code—ASP.NET MVC and jQuery have tidier syntaxes for emitting and parsing it.

Animations and Other Graphical Effects

Until recently, most sensible web developers avoided fancy graphical effects such as animations, except when using Adobe Flash. That's because DHTML's animation capabilities are primitive (to say the least) and never quite work consistently from one browser to another. We've all seen embarrassingly amateurish DHTML "special effects" going wrong. Professionals learned to avoid it.

However, since script.aculo.us appeared in 2005, bringing useful, pleasing visual effects that behave properly across all mainstream browsers, the trend has changed.[95] jQuery gets in on the action, too: it does all the basics—fading elements in and out, sliding them around, making them shrink and grow, and so on—with its usual slick and simple API. Used with restraint, these are the sort of professional touches that you do want to show to a client.

The best part is how easy it is. It's just a matter of getting a wrapped set and sticking one or more "effects" helper methods onto the end, such as .fadeIn() or .fadeOut(). For example, going back to the previous stock quotes example, you could write

$("form[action$='GetQuote']").submit(function () {
    $.ajax({
        url: $(this).attr("action"),
        type: "post",
        data: $(this).serialize(),
        success: function (stockData) {
            $("#openingPrice").html(stockData.OpeningPrice).hide().fadeIn();
            $("#closingPrice").html(stockData.ClosingPrice).hide().fadeIn();
            $("#stockRating").html(stockData.Rating).hide().fadeIn();
        }
    });
    return false;
});

Note that you have to hide elements (e.g., using hide()) before it's meaningful to fade them in. Now the stock quote data fades smoothly into view, rather than appearing abruptly, assuming the browser supports opacity.

Besides its ready-made fade and slide effects, jQuery exposes a powerful, general purpose animation method called .animate(). This method is capable of smoothly animating any numeric CSS style (e.g., width, height, fontSize, etc.)—for example:

$(selector).animate({fontSize : "10em"}, 3500); // This animation takes 3.5 seconds

If you want to animate certain nonnumeric CSS styles (e.g., background color, to achieve the clichéd Web 2.0 yellow fade effect), you can do so by getting the official Color Animations jQuery plug-in (see http://plugins.jquery.com/project/color).

jQuery UI's Prebuilt UI Widgets

A decade ago, when ASP.NET Web Forms was being designed, the assumption was that web browsers were too stupid and unpredictable to handle any kind of complicated client-side interactivity. That's why, for example, Web Forms' original <asp:calendar> date picker renders itself as nothing but plain HTML, invoking a round trip to the server any time its markup needs to change. Back then, that assumption was pretty much true, but these days it certainly is not true.

Nowadays, your server-side code is more likely to focus just on application and business logic, rendering simple HTML markup (or even acting primarily as a JSON or XML web service). You can then layer on rich client-side interactivity, choosing from any of the many open source and commercial platform-independent UI control suites. For example, there are hundreds of purely client-side date picker controls you can use, including ones built into jQuery and ASP.NET AJAX. Since they run in the browser, they can adapt their display and behavior to whatever browser API support they discover at runtime. The idea of a server-side date picker is now ridiculous; pretty soon, we'll think the same about complex server-side grid controls. As an industry, we're discovering a better separation of concerns: server-side concerns happen on the server; client-side concerns happen on the client.

The jQuery UI project (see http://ui.jquery.com/), which is built on jQuery, provides a good set of rich controls that work well with ASP.NET MVC, including accordions, date pickers, dialogs, sliders, and tabs. It also provides abstractions to help you create cross-browser drag-and-drop interfaces.

Example: A Sortable List

jQuery UI's .sortable() method enables drag-and-drop sorting for all the children of a given element. If your view is strongly typed for IEnumerable<MountainInfo>, you could produce a sortable list as easily as this:

<b>Quiz:</b> Can you put these mountains in order of height (tallest first)?

<div id="summits">
    <% foreach(var mountain in Model) { %>
        <div class="mountain"><%: mountain.Name %></div>
    <% } %>
</div>

<script>
    $(function() {
        $("#summits").sortable();
    });
</script>

Note

To make this work, you need to download and reference the jQuery UI library. The project's home page is at http://ui.jquery.com/—use the web site's "Build your download" feature to obtain a single .js file that includes the UI Core, Draggable, and Sortable modules (plus any others that you want to try using), add the file to your /Scripts folder, and then reference it from your master page or ASPX view page.

This allows the visitor to drag the div elements into a different order, as shown in Figure 14-16.

jQuery UI's .sortable() feature at work

Figure 14-16. jQuery UI's .sortable() feature at work

The visitor can simply drag the boxes above and below each other, and each time they release one, it neatly snaps into alignment beside its new neighbors. To send the updated sort order back to the server, add a <form> with a submit button, and intercept its submit event:

<% using(Html.BeginForm()) { %>
    <%: Html.Hidden("chosenOrder") %>
    <input type="submit" value="Submit your answer" />
<% } %>
<script>
    $(function() {
        $("form").submit(function() {
            var currentOrder = "";
            $("#summits div.mountain").each(function() {
                currentOrder += $(this).text() + "|";
            });
            $("#chosenOrder").val(currentOrder);
        });
    });
</script>

At the moment of submission, the submit handler fills the hidden chosenOrder field with a pipe-separated string of mountain names corresponding to their current sort order. This string will of course be sent to the server as part of the POST data.[96]

Summarizing jQuery

If this is the first time you've seen jQuery at work, I hope this section has changed the way you think about JavaScript. Creating sophisticated client-side interaction that supports all mainstream browsers (downgrading neatly when JavaScript isn't available) isn't merely possible; it flows naturally.

jQuery works well with ASP.NET MVC, because the MVC Framework doesn't interfere with your HTML structure or element IDs, and there are no automatic postbacks to wreck a dynamically created UI. This is where MVC's "back to basics" approach really pays off.

jQuery isn't the only popular open source JavaScript framework (though it seems to get most of the limelight at present). You might also like to check out Prototype, MooTools, Dojo, Yahoo User Interface Library (YUI), or Ext JS—they'll all play nicely with ASP.NET MVC, and you can even use more than one of them at the same time. Each has different strengths: Prototype, for instance, enhances JavaScript's object-oriented programming features, while Ext JS provides spectacularly rich and beautiful UI widgets. Dojo has a neat API for offline client-side data storage. Reassuringly, all of those projects have attractive Web 2.0-styled web sites with lots of curves, gradients, and short sentences.

Summary

This chapter covered two major ways to implement Ajax functionality in an ASP.NET MVC application. First, you saw ASP.NET MVC's built-in Ajax.* helpers, which are very easy to use but have limited capabilities. Then you got an overview of jQuery, which is enormously powerful but requires a fair knowledge of JavaScript.

Having read this much of the book, you've now learned about almost all of the MVC Framework's features. What's left is to understand how ASP.NET MVC fits into the bigger picture, such as how to deploy your application to a real web server, and how to integrate it with core ASP.NET platform features. This begins in the next chapter, where you'll consider some key security topics that every ASP.NET MVC programmer needs to know about.



[83] Ajax stands for asynchronous JavaScript and XML. These days, few web applications transmit XML—we usually prefer to send data in HTML or JSON format—but the technique is still known as Ajax.

[84] This includes Internet Explorer 6.0, Firefox 1.5, Opera 9.0, Safari 2.0, and later versions of each.

[85] Notice that IsMvcAjaxRequest() is a method, not a property, because C# 3 doesn't have a concept of extension properties.

[86] JavaScriptResult sets the response's content-type header to application/x-javascript. The Ajax.* helpers specifically look for that value.

[87] It's available for commercial and personal use under both the MIT and GPL licenses.

[88] Currently, it supports Firefox 2.0+, Internet Explorer 6+, Safari 3+, Opera 9+, and Chrome 1+.

[89] See http://trends.builtwith.com/javascript/JQuery.

[90] In JavaScript terms, that is to say $ == jQuery (functions are also objects). If you don't like the $() syntax—perhaps because it clashes with some other JavaScript library you're using (e.g., Prototype, which also defines $)—you can disable it by calling jQuery.noConflict().

[91] Better still, give them a way of undoing the action even after it has been confirmed. But that's another topic.

[92] The element you parse out of the response by calling $("#summits", response) must not be a direct child of the <body> element, or it won't be found. That's rarely a problem, but if you do want to find a top-level element, you should replace this with $(response).filter("div#summits").

[93] In the same way that new { OpeningPrice = 556.94M, ClosingPrice = 558.20M, Rating = "A+" } is C# source code.

[94] Here, I'm using the term web service to mean anything that responds to an HTTP request by returning data (e.g., an action method that returns a JsonResult, some XML, or any string). With ASP.NET MVC, you can think of any action method as being a web service. There's no reason to introduce the complexities of SOAP, ASMX files, and WSDL if you only intend to consume your service using Ajax requests.

[95] script.aculo.us is based on the Prototype JavaScript library, which does many of the same things as jQuery. See http://script.aculo.us/.

[96] Alternatively, you can use jQuery UI's built-in .sortable("serialize") function, which renders a string representing the current sort order. However, I actually found this less convenient than the manual approach shown in the example.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.29.148