In this chapter:
At the heart of Ajax programming is the ability to make asynchronous calls from the browser to the server. Establishing this dialogue eliminates the need for the browser to reload as a result of each request or user interaction. Instead, relevant data can be exchanged in the background while updates to the page are applied incrementally from the browser. Web pages that leverage this technique remain responsive, and the user experience is greatly improved.
In chapter 1, you got a glimpse into how this type of programming works with ASP.NET AJAX—we called this approach the client-centric development model. This model grants you more control over the application by moving the logic from the server into the browser. This shift from traditional ASP.NET development means the server is primarily used for data rather than application logic and data together.
This chapter will explain how you can make asynchronous network calls from JavaScript using the ASP.NET AJAX framework. We’ll explore the Microsoft Ajax Library classes that make asynchronous communication possible. In addition, we’ll unveil how to make calls to ASP.NET Web Services, both local and external, from client-side script. Let’s begin with what will be the most likely scenario you’ll leverage when making asynchronous calls: working with ASP.NET Web Services.
A website is a perfect example of the client/server architecture. Each instance of a browser (the client) can send requests to a server for data and content. When the client initiates a request to a known remote server to execute a procedure or subroutine, it’s often called a remote procedure call (RPC). Working closely with ASP.NET Web Services, the ASP.NET AJAX framework significantly simplifies the effort it takes to execute RPC patterns from JavaScript. In simpler terms, the framework makes it easy for you to communicate with Web Services from JavaScript.
Before we dive into working with Web Services, let’s take a few moments to explain how communicating with RPC services works and how these services differ from another style called Representation State Transfer (REST).
You communicate with an RPC service using commands defined through methods. This is similar to how you interact with a normal object from a library. For example, suppose an RPC application defines a method called GetStoreSpecials. A consumer of that service can then communicate with it like so:
storeService = new StoreService("aspnetajaxinaction.com:42"); storeService.GetStoreSpecials();
REST services expose their communication endpoints slightly differently. They expose objects as resources or nouns, which have more of an emphasis on diversity. For the same functionality, a REST service typically offers a resource this way: http://ajaxinaction.com/specials/. A caller in this scenario then accesses the application in a fashion similar to this:
storeResource = new StoreResource("http://ajaxinaction/specials/"); storeResource.GetStoreSpecials();
We’re giving you this overview of these two service models to provide the context in which communication works in ASP.NET AJAX. As we walk through the first set of examples, you’ll notice how you declare and work with RPC-like services for applications. It’s interesting to note that under the hood, the communication layer in the framework is implemented with REST-like patterns. More of this will make sense as we proceed.
An entire book could be dedicated to topics such as REST and RPC services. We provide a brief introduction here, but it’s in no way a thorough explanation. For more information about REST services, see http://rest.blueoxen.net/cgi-bin/wiki.pl?FrontPage. You can find a helpful resource about RPC here: http://www.cs.cf.ac.uk/Dave/C/node33.html.
Let’s get into some code and begin working with the framework. We’ll start with a simple web service that you can expose to the client-side script.
Let’s start with a clean slate and create a new Ajax-enabled website from Visual Studio (see chapter 1 for an example). Selecting this option updates the web.config file with all the settings and references you need to get going. The next step is to add a local web service to the site. You can accomplish this by choosing the Web Service option in the Add New Item dialog (see figure 5.1).
To keep everything in one place and for clarity, deselect the Place Code in Separate File option. Building on the Starbucks example in chapter 1 (more on this soon), you’ll name the service StarbucksService.asmx. You’ll target this service from the client to retrieve relevant data in the examples.
Earlier, we explained the nature of asynchronous operations by telling a story of ordering a beverage from a coffee shop. In brief, we associated placing an order at the shop with making a request to a service. We then likened the processing of that order to an asynchronous operation in which, due to its nature, we were informed of the operation’s status and completion at another time. For the remainder of this section, we’ll use this tale as the premise for the examples. If you aren’t familiar with how an asynchronous operation behaves, please take a moment to visit the story in chapter 1 for a high-level explanation.
Listing 5.1 shows the beginnings of this service and how it’s exposed to the client-side script.
Exposing a web service to the client in ASP.NET AJAX is done with a few simple steps. The first step, which isn’t required, is to include the namespace for the script services in the framework. This serves as a shortcut so you don’t have to fully qualify each attribute and type used from the library. Next, you must decorate the class for the service with the ScriptService attribute, defined in the System.Web.Script.Services namespace. The service and its web methods are now ready for remote calls from the browser.
Currently, the service contains only one method: GetLocationCount, which returns the number of stores in a specified ZIP code. Because this is strictly demo code, we hard-coded a few examples and values in order to get results to experiment with.
The 1.0 release of the ASP.NET AJAX framework doesn’t support integration with Windows Communication Foundation (WCF). In earlier Community Technology Previews (CTPs), when the project was known by the codename Atlas, WCF integration was supported experimentally. In the next version of the .NET Framework, currently codenamed Orcas, WCF support will return.
To validate your work so far, open a browser window and direct it to the service’s .asmx file. As expected, you see the generated summary page that you’ve become accustomed to with normal ASP.NET Web Services. Figure 5.2 shows the summary page and the single method it currently exposes.
Everything appears normal so far, but this isn’t your typical web service. If you append /js to the end of the URL, such as http://www.samplewebsite.com/sampleservice.asmx/js, then instead of seeing the friendly generated page for the service, you’re presented with JavaScript content that represents the client-side proxy for this service. (Firefox displays the script in the page, and Internet Explorer 7 prompts you to save the contents into a local file.) We’ll dig deeper into how this is made possible soon. The important thing to remember right now is that you get a set of JavaScript functions that you can leverage to call the web methods from the script. This JavaScript code, or proxy, is also known as a web service proxy.
The next logical step is to add a page to the site that interacts with this service.
The first step in Ajax-enabling a page is to add the ScriptManager control. Remember, the ScriptManager is the brains of an Ajax page because its responsibilities primarily include managing and deploying scripts to the browser. In this case, you want to leverage the ScriptManager so the page can use the web service proxy you just generated. Listing 5.2 shows how adding a reference to the local Web Service makes this possible.
<asp:ScriptManager ID="ScriptManager1" runat="server"> <Services> <asp:ServiceReference Path="~/StarbucksService.asmx" InlineScript="true" /> </Services> </asp:ScriptManager>
The Services property of the ScriptManager contains a collection of ServiceReference objects. A ServiceReference is a reference to a local service on the site. Adding this reference informs the ScriptManager that you would like to include the web service proxy from the service on the page.
The Path for the service reference is set to the .asmx file on the site. By default, the InlineScript property of the reference is set to false. However, in this case it’s set to true to demonstrate how the web service proxy will be downloaded, in the page, to the browser. When set to false, the JavaScript for the proxy is instead downloaded to the browser separately.
Using a debugging tool called Firebug (see appendix B for details) from the Firefox browser, you can see the client-side proxy generated for the page (see figure 5.3).
Now that the infrastructure is in place, you can begin making calls to the service. To invoke the single method in the service, add a text box and button to the page to provide the user with an interface for passing in data. The markup portion for this example is presented in listing 5.3.
<div> <input id="Location" type="text" /> <input id="GetNumLocations" type="button" value="Get Count" onclick="getLocations()" /> <div id="NumLocations"></div> </div>
Notice how the onclick event for the button is assigned to the JavaScript function getLocations. In this function, you read the value from the text box and pass it along to the web method.
Making a web method call from JavaScript is similar to calling a method from a library in .NET, except for a few differences that we’re about to uncover. Listing 5.4 demonstrates how you make the call to the service for retrieving the number of locations in a ZIP code.
function getLocations(){ var zip = $get("Location").value; AspNetAjaxInAction.StarbucksService.GetLocationCount(zip, onGetLocationSuccess, onGetLocationFailure, "<%= DateTime.Now %>"); }
Prefixed with the namespace and then the name of the service, StarbucksService, a call to the GetLocationCount method defined in the .asmx file is invoked along with a few extra parameters. Let’s carefully examine each of these extra parameters.
Passed in to the first parameter is the value in the text box that you retrieved by calling $get("Location").value in the previous line. The second parameter is the name of the callback function, onGetLocationSuccess, which informs you when the method has successfully completed. Optionally, passed in to the third parameter is the name of another callback function that is invoked if anything goes wrong during the processing of the request. This can include a timeout on the request, loss of connectivity, and a number of other possibilities.
The last parameter provides a mechanism for passing along user context that can be retrieved from either of the callback functions. This example passes in the current time, but any JavaScript object will do. The Microsoft Ajax Library maintains this context for you on the client so that it’s conveniently available later when the callbacks are invoked. After the call is made, you wait for either callback function to be invoked.
When the call successfully completes, the function you specified—onGetLocationSuccess—is called, and you can update the page with its return value:
function onGetLocationSuccess(result, context, methodName){ $get("NumLocations").innerHTML = result + " location(s) found."; }
Three parameters are passed in to the callback function. The first, often called the result parameter, returns the results of the web method. For this example, it’s an integer that signifies the number of store locations in the ZIP code. The second parameter is the user context you optionally passed in when you called the method. The last parameter contains the name of the client-side method that initiated this callback. Because the same callback function can be used for different method calls (doing so is common), this parameter can be handy for determining where the call originated and applying additional custom logic.
Everything is straightforward so far, but what happens when an error occurs on the server or the call fails to return successfully due to network complications? In this scenario, the second callback function, onGetLocationFailure, is called:
function onGetLocationFailure(error, context, methodName){ var errorMessage = error.get_message(); $get("NumLocations").innerHTML = errorMessage; }
Inspecting the parameters in the callback, notice that the second and third items are the same as in the successful callback routine earlier. The difference this time is the result parameter (the first parameter), which returns an error object. For this occasion, you can retrieve the error message by calling error.get_message() to update the UI accordingly.
The last thing we’ll touch on to round off your basic understanding of making JavaScript calls to services is the issue of timeouts.
When you’re calling a web service proxy from JavaScript, you sometimes have to take into consideration the amount of time it takes for a request to process. In some cases, you want the call to return immediately, so adjusting the timeout for a shorter interval is preferable. In other instances, a longer timeout that grants the server sufficient time to process the request is better suited. The client-side proxies in ASP.NET AJAX expose a property called timeout, which allows you to adjust the interval in milliseconds:
AspNetAjaxInAction.StarbucksService.set_timeout(1000);
If a response isn’t received before the timeout elapses, an exception is generated on the client and the failure callback function is invoked. The error object passed to the callback contains the client-generated exception for a timeout. We’ll discuss how to handle errors in a moment.
Determining the right timeout interval can be tricky. Ideally, you want to provide the user with feedback as soon as possible, which means short timeouts are preferred. You may want to consider reissuing the request if it initially fails. On the other hand, you want to give the Web Service adequate time to process the request. This time can vary between services and can depend on how busy the service is. Sometimes, it’s beneficial to implement a more complex algorithm that issues a short timeout at first and then adjusts itself with a slightly longer timeout interval the next time. You may have to manage and refine the timeout interval for processing a request when you’re working with complex scenarios.
So far, we’ve covered the basics of working with ASP.NET Web Services. We have a lot more to cover, especially relating to working with complex types.
We’ve walked through the simplest scenario possible when working with ASP.NET Web Services: calling a method that returns an integer. But applications work with more complex, custom types that closely resemble entities in the real world. In this section, we’ll work with these complex types and walk through a series of exercises to demonstrate how you can access them and instantiate them in the script.
With the help of a good book (wink), word around town is that you’ve become quite the Ajax developer. Management of a well-known coffee shop has asked you to update some of their Web Services so their developers can add more interaction to the company’s home page. Your first task is to add a web method that returns the latest deals on the most popular beverages.
You begin by creating a server-side object called Beverage. Keeping things simple, the object has only a few properties: a name, a description, and a cost. Included in the class’s implementation is an overloaded constructor that initializes the object with the passed-in properties. Listing 5.5 shows the implementation for this custom type.
using System; namespace AspNetAjaxInAction { public class Beverage { public Beverage() { } public Beverage(string name, string desc, double cost) { this.name = name; this.description = desc; this.cost = cost; } private string name; public string Name { get { return this name; } set { this.name = value; } } private string description; public string Description { get { return this description; } set { this.description = value; } } private double cost; public double Cost { get { return this.cost; } set { this.cost = value; } } } }
Next, you add to the Web Service a method called GetDeals, which returns a collection of beverages (see listing 5.6).
using System.Collections.Generic; ... [WebMethod] public List<Beverage> GetDeals() { List<Beverage> beverages = new List<Beverage>(); // Hard-coded for testing Beverage b1 = new Beverage("House Blend", "Our most popular coffee", 2.49); Beverage b2 = new Beverage("French Roast", "Dark, bold flavor", 2.99); beverages.Add(b1); beverages.Add(b2); return beverages; }
Let’s examine the newly added GetDeals method. Notice how you import the System.Collection.Generic namespace at the top. You do this because you’d like to use Generics (a .NET 2.0 feature that is similar to templates in C++) in the return type as opposed to a normal array. On the client side, this doesn’t matter, because it’s serialized into an array anyway. On the server side, however, Generics provides an easy way to manage typesafe lists.
Generics aren’t required for this solution; you can just as easily set the method to return an array of the Beverage type (Beverage[]). But unless you’re targeting both .NET 1.1 and .NET 2.0, you should take advantage of Generics when possible. If you aren’t familiar with Generics, devoting some time to learning about its benefits would be a worthwhile investment. For C#, see http://msdn2.microsoft.com/en-us/library/ms379564(vs.80).aspx. For VB.NET, see http://msdn2.microsoft.com/enus/library/ms379608(vs.80).aspx.
A close look at the implementation of the method reveals that a hard-coded list of beverages is created and returned to the caller. The question now is, how can the client-side script handle this new type?
By default, the ASP.NET Web Services used by the Ajax framework use the JSON (see chapter 3) data format for the transfer of data between the client and server. This means the value is first serialized with a JSON serializer before it’s written in the response’s payload. One of the key reasons JSON is used is because of its natural integration with JavaScript and its lightweight nature. (You can convert JSON into a JavaScript object by passing it into the eval method.)
When the result reaches the callback in the JavaScript code, you’re given an object that you can manipulate and work with like a normal .NET object. To demonstrate, let’s put this all together by calling the GetDeals method from JavaScript (see listing 5.7).
Listing 5.7 begins with the declaration of a button on the form that you use to kick off the request in a function called getDeals. From there, you call the GetDeals method on the server and assign callback functions for both success and failure scenarios.
If the call returns successfully, you instantiate an instance of the client StringBuilder object and format the result. Notice how the properties you declared in the server class (Name, Description, and Cost) are accessed from the script to format the message. All the work of serializing and deserializing the object is transparent to you, and you didn’t have to do anything extra to introduce the new object into the proxies.
As soon as the browser receives the response, the Microsoft Ajax runtime processes it and uses the client-side serializer (the Sys.Serialization.JavaScriptSerializer class) to deserialize the JSON sent by the server. The runtime then invokes the callback that you set to process the results. This lets you access and work with the result as an object, like the one defined on the server.
Let’s look at the output. Figure 5.4 demonstrates the results of your efforts up to now.
If you insert a call to Sys.Debug.traceDump(result) from the callback function for the GetDeals method, you can use the Firebug tool to inspect what comes back from the server (see figure 5.5).
More details about debugging and using tools such as Firebug and Web Developer Helper are provided in appendix B. We encourage you to become familiar with these tools and leverage them when you’re authoring rich-client applications.
The client is thrilled with your work so far, particularly the way the object you defined on the server can be used seamlessly in the browser as well. This prompts them to ask if it’s possible to instantiate an instance of a server-side class from the client.
Because the Beverage type is used in the service’s GetDeals method, the client proxies already include a definition for it. This happens when the proxies are generated and the type is resolved by the Ajax runtime. Creating and initializing an instance of the Beverage type from JavaScript looks similar to how you would do this in .NET code:
var bev = new AspNetAjaxInAction.Beverage(); bev.Name = "Holiday Blend"; bev.Description = "A warm and spicy blend."; bev.Cost = "2.55";
What about classes that aren’t used in the Web Service? In some cases, the client would like to use the same class they defined on the server, in the browser as well. It seems redundant to have to define the same object in JavaScript because it isn’t used by the service.
To demonstrate how you can resolve this situation, let’s create another class on the server called Employee. For simplicity, this class also has three basic properties: first name, last name, and title. Listing 5.8 shows the implementation for the class.
using System; namespace AspNetAjaxInAction { public class Employee { public Employee() { } private string first; public string First { get { return this.first; } set { this.first = value; } } private string last; public string Last { get { return this.last; } set { this.last = value; } } private string title; public string Title { get { return this.title; } set { this.title = value; } } } }
The goal is to instantiate and update the object in JavaScript as you did with the Beverage object previously. Because the class hasn’t been used in any method calls, the Web Service isn’t aware that you’d like to include this class in the proxies. To enlighten the Web Service about your intentions, you can leverage the GenerateScriptType tag. If you apply this tag to the Web Service class, along with the type of class you’d like to include, it too will be supported in the web service proxy. Listing 5.9 shows how the Web Service class is updated with the script-type declaration of the Employee class.
[ScriptService] [GenerateScriptType(typeof(Employee))] [WebService(Namespace = "http://aspnetajaxinaction.com/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class StarbucksService : System.Web.Services.WebService { ...
This is all you need to do to provide support for instantiating a server-side object on the client. To prove that this class can be created and manipulated from JavaScript, add the following lines of markup and script to the page:
<hr /> <div> <input id="CreateEmployee" type="button" value="Instantiate Employee" onclick="createEmployee()" /> </div> ... function createEmployee(){ var emp1 = new AspNetAjaxInAction.Employee(); emp1.First = "Frank"; emp1.Last = "Rizzo"; emp1.Title = "Principal"; }
Without the GenerateScriptType annotation in the Web Service class, a runtime exception would occur when you try to instantiate the Employee object. Instead, you’re able to create an instance and update its properties accordingly.
Making asynchronous requests from JavaScript to a Web Service is pretty easy. What isn’t as easy is changing the way these request are submitted. Let’s take a closer look at the types of requests we’re talking about.
So far, all the calls you’ve made to the Web Service have used the HTTP POST verb. As a security measure, which we’ll delve into in a minute, ASP.NET AJAX accepts these types of requests only from the browser by default. To accommodate an HTTP GET request, you’re forced to explicitly adorn a method with the ScriptMethod attribute as well as set its UseHttpGet property to true. This subtle but conscious declaration prevents you from inadvertently letting the browser invoke methods with the HTTP GET verb. Listing 5.10 demonstrates how to update one of the existing methods, GetDeals, with HTTP GET capabilities.
[ScriptMethod(UseHttpGet=true)] [WebMethod] public List<Beverage> GetDeals() { ...
Why is HTTP GET disabled by default? The primary reason is to avoid compromising security in Ajax applications. To help you understand the kind of security we’re talking about, we’ll describe how JSON hijacking works.
A common approach for JSON hijacking is to introduce into a page a malicious script that invokes an HTTP GET request, like so:
<script type="text/javascript" src="someReallyEvilScript.js"> </script>
Because the script is included on the page, it evades the origin policy that browsers enforce. This policy is put in place to limit objects like XMLHttpRequest from calling URLs in the same domain. This exploit leaves the JSON payload open for viewing and manipulation of the script. Thankfully, the ASP.NET AJAX framework provides more than one barrier for stopping this problem (a technique known as security in depth).
The first layer of security for this scenario forces you to explicitly enable HTTP GET on a method, as we just covered. Second, validation against the Content-Type header field of the request is applied to ensure that it’s set to application/json. It’s interesting to note that when browsers parse external scripts that are included on a page, the content type is never set to application/json when making the request. If any of these conditions aren’t met (HTTP GET settings or the application/json content type), then the request is promptly rejected.
Before we wrap up this section on working with Web Services, we’ll explore one more approach. It involves making JavaScript calls to methods on a page, instead of to a Web Service.
An interesting feature in ASP.NET AJAX is the ability to call, from JavaScript, methods that are declared in the ASP.NET page itself. Because these methods are declared on a page, not from a Web Service, they’re appropriately called page methods. To demonstrate how this works, let’s add a simple static method called HelloEmployee to the page. This method takes as a parameter an instance of the Employee class you created earlier. The method returns to the caller a formatted greeting:
[WebMethod] public static string HelloEmployee(AspNetAjaxInAction.Employee emp) { return string.Format("Hello {0} {1}.", emp.First, emp.Last); }
Notice how the method is decorated with the WebMethod attribute (defined in the System.Web.Services namespace), similar to public methods in a Web Service. This required attribute must be adorned on any methods you want to expose as a page method.
In the .aspx page, you enable support for these types of methods by setting the EnablePageMethods property of the ScriptManager to True. By default, this setting isn’t enabled, and any static web methods on the page are omitted from the web service proxy:
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="True"> <Services> <asp:ServiceReference Path="StarbucksService.asmx" InlineScript="true" /> </Services> </asp:ScriptManager>
To complete this example, you need to call the method from JavaScript and process the response. You do so in much the same manner as the previous asynchronous requests, but this time the calls are prefixed with PageMethods as opposed to the name of the service class. To demonstrate, let’s extend the createEmployee function you wrote earlier to pass in the Employee instance to the HelloEmployee method (see listing 5.11).
You call the static method HelloEmployee that is declared on the page. Passed into the method is an instance of the Employee class that you instantiated on the browser. When the code is executed and the results are returned, an alert dialog is displayed to greet the employee (see figure 5.6).
Page methods offer an alternative to creating a local Web Service for a site. One of the caveats is that only the script from the current page can access the method, as opposed to offering it to other pages on the site. You can look at it as a private web method for that page.
You should now have a solid understanding of how to work with Web Services and JavaScript. The next section will bring you closer to what goes on behind the scenes with asynchronous network calls.
In this section, we’ll examine the network layer, also known as the asynchronous communication layer, in the Microsoft Ajax Library. Briefly, this layer of the Ajax stack provides a set of client classes that abstract away from the client any browser-specific discrepancies for network communication. This enables you to write consistent, solid code for sending asynchronous requests to a web server. Let’s begin by examining a simple request and the components that glue it together.
The process of sending an HTTP request with the Microsoft Ajax Library involves three objects:
To grasp the role of these objects, let’s put together a quick example that makes a simple request.
Suppose you have a file called message.txt on the site. To request the contents of this file from JavaScript, you can use code like that shown in listing 5.12.
var request = new Sys.Net.WebRequest(); request.set_url("message.txt"); request.add_completed(onRequestComplete); request.invoke();
The first step in putting together a request is to create an instance of the Sys.Net.WebRequest object. Then, you set the url property of the request to the file on the server. Next, you add an event handler for when the request completes by calling the add_completed function and passing in the name of the routine. The final statement in listing 5.12 is a call to the invoke method, which is responsible for issuing the asynchronous request.
The add_completed function should be called before the invoke method on the WebRequest instance. If the browser has the message.txt file already in its cache, you don’t need to issue an HTTP request to the server. In this case, the request completes synchronously, and the onRequestComplete handler is called before the invoke method returns.
Let’s look now at the callback routine, onRequestComplete. Here, you receive the contents of the file requested from the server:
function onRequestComplete(executor, eventArgs) { alert(executor.get_responseData()); }
The onRequestComplete function is called with two parameters. The first, executor, is of type Sys.Net.WebRequestExecutor, and contains all the information about the status of the asynchronous request. The second, eventArgs, is always set to Sys.EventArgs.Empty—an object that represents the absence of event arguments.
Sys.EventArgs.Empty plays the same role as the System.EventArgs.Empty object that is passed to event handlers in the .NET framework to indicate the absence of event arguments.
To retrieve the contents of the file, you can call the get_responseData method of the executor object. If the response completes successfully, the content of the message.txt file is returned. In section 5.2.4, we’ll examine what happens when a request fails and how to handle it cleanly.
This executor object is important. Let’s discuss its function in the process.
The executor object that you accessed in the earlier example is an instance of the Sys.Net.XMLHttpExecutor class. In turn, this class inherits from Sys.Net.WebRequestExecutor, which acts as an abstract class. By overriding the implementation of the executeRequest method, you can specify how an HTTP request is sent from script. For example, the default executor, Sys.Net.XMLHttpExecutor, sends a request using the XMLHttpRequest object. Other types of executors can be created to implement different techniques for sending asynchronous requests to the server.
At the moment, the XMLHttpExecutor is the only executor provided by the Microsoft Ajax Library. Previous CTPs included other executors, such as the IFrameExecutor. However these executors were omitted from the final release for quality and security reasons.
The executor object provides all the information you need to know about the response sent by the web server. If you’re expecting data in an XML format, use the get_xml method to retrieve the response in the form of an XML DOM object. Data returned in a JSON format can be retrieved with the get_object method. The executor also offers methods that you can use to examine the status code and text of each response:
var statusCode = executor.get_statusCode(); var statusText = executor.get_statusText();
Facilitating all this interaction is a single object on the client called the WebRequestManager. To help you understand how all the pieces fit together, we’ll continue our exploration by briefly looking at how this object is used and where it fits.
The WebRequestManager is an instance of the Sys.Net._WebRequestManager class. When the Microsoft Ajax runtime is loaded, the instance is created and stored in a global JavaScript variable called Sys.Net.WebRequestManager.
When the invoke method is called on a WebRequest object, the request is passed to the WebRequestManager. Here, checks are made to determine the associated implementation of the executor object for the request. If an assigned executor isn’t found, then the default XMLHttpExecutor is used. At this point, the WebRequestManager calls the executeRequest method on the executor object to launch the request.
This leads us to errors and how you should handle them correctly and efficiently. In the next section, you’ll put together a useful error-handling mechanism that can be added to your toolbox for future Ajax development.
The onRequestComplete function that you used earlier has an obvious problem: It doesn’t check for errors. It assumes that the request always succeeds and that the response always contains the expected data. Loss of network connectivity, an overloaded server, and runtime exceptions are a few of the reasons an error can occur. Too many things are not under your control and can have a negative impact on the outcome of a request. In such cases, you need to inform the user properly that an error occurred, or at least have logic in place to manage the error efficiently.
Errors fall into two categories: server-returned HTTP errors and non-HTTP errors. You can have a server-returned HTTP error when the request successfully reaches the server but fails to process it. A valid example is when an ASP.NET application throws an exception during the processing of the request, such as attempting to access an object with no reference. When this happens, you can obtain the exact error from the HTTP status code returned by the server. The response’s payload also contains the error description. Table 5.1 summarizes the status codes you’re interested in.
Status code range |
Meaning |
---|---|
100-199 | Informational status codes. These status codes don’t normally arise during ASP.NET AJAX development and can be ignored. |
200-299 | The request was received and successfully processed. |
300-399 | The request needs to be redirected. The most common code in this range is 302, which is sent when Response.Redirect is called from ASP.NET code. |
400-499 | The request contains an error. A common error code is 404, which indicates that the resource (file) wasn’t found on the server. |
500-599 | The request was valid, but the server failed to process the request. The most common error code in this range is 500, which is returned by ASP.NET when an exception occurs during request processing. |
Based on your knowledge of the HTTP status codes, you can modify the onRequestComplete method as shown in listing 5.13.
This handles most exceptions, but what about making the code reusable? And what about timeouts and requests that are aborted? Perhaps it makes sense to make the error handling more granular. This way, more information is available in case additional logic is needed for specific types of errors.
Listing 5.14 demonstrates a snippet of code that extends the WebRequestExecutor object with a function called checkError.
Let’s walk through the code in the checkError routine to learn more about the other ways of investigating errors. First, the variable e is declared and initialized to null. If no error is found, the fact that this variable remains null signifies to the caller that the request succeeded. Next, you check to see if the request was aborted by checking the aborted property of the executor object. If this condition is true, then an Error object is created along with the status code and text from the WebRequestExecutor object.
If the request wasn’t aborted, the next check queries the timedOut property to determine if the server took too long to process the request. The final check examines the status code. If the code’s value doesn’t fit between 200 and 300, then you create an Error object accordingly.
Connection failures are handled differently depending on the browser. Internet Explorer, for example, sometimes returns a status code that is greater than 600 with text that conveniently says Unknown. Firefox throws an exception when get_statusCode is called. Opera prefers to return a status code of zero and empty status text. As you can imagine, a reusable function can come in handy.
The checkError function can become part of your toolbox moving forward. Because it extends the WebRequestExecutor object through the prototype property (see chapter 3 for details), you can use it conveniently in future projects.
This concludes our general overview of how to communicate with local services from the client. In some cases, you want to reach out to external services—in the same domain or outside. In the next section, we’ll address a few approaches that work with ASP.NET AJAX.
Today’s modern browsers impose security restrictions on network connections that include calls to the XMLHttpRequest object. These restrictions prevent a script or application from connecting to a web server other than the one the page originally came from. These calls are referred to as cross-domain requests. Figure 5.7 illustrates this basic restriction.
The rationale is simple: If it’s possible to make such a call, then it’s possible for a rogue website to access sensitive data from other websites. For example, a web page in http://www.rogue-domain.com can use the XMLHttpRequest object and access all of the user’s e-mails from http://mail.live.com. Fortunately, such an action isn’t permitted by the XMLHttpRequest object in its default configuration.
Even though cross-domain calls aren’t permitted by the XMLHttpRequest object in its default configuration, the user can change the browser settings to make certain cross-domain calls succeed. However, asking users to change their browser settings so they can access your website is generally a bad practice.
The following types of calls are considered cross-domain requests:
A few safe, recommended ways are available to access data across domains. In the following sections, we’ll discuss a number of different approaches that bypass the cross-domain limitation.
Even though the browser doesn’t allow XMLHttpRequest calls across domains, it allows a website to load scripts hosted in a different domain. For instance, a web page in http://www.mywebsite.com can request a JavaScript file from http://www.yourwebsite.com using the following markup:
<script src="http://www.yourwebsite.com/somecoolscript.js" type="text/javascript"></script>
As usual, the src attribute of the script tag contains the URL of the script file to request. In some cases, parameters are passed in to the query string to retrieve more specific data. The handler for that URL on the server then usually parses the query string to generate JavaScript code based on the parameters passed in. The generated script sent from the server can even take actions on the client. If you wanted to make these calls asynchronous, you could create a dynamic script tag using JavaScript and the DOM instead of declaratively placing it on the page.
This method, widely used in Google Maps, Virtual Earth, and the Yahoo JSON APIs, is in many ways limited because the scripts are always requested with the HTTP GET verb. This confines the request to making queries for data with no support for altering the state of a local server (a possibility that a HTTP POST request offers). This comes in handy if you’re providing such services, but it falls short of providing additional features for consumers.
You can resolve this situation by introducing something in the middle—a proxy, if you will—that facilitates communication between the local site and an external service. Let’s explore this technique in the next section.
Let us recap what you know so far. First, you learned that including scripts in pages from other domains allows you to retrieve data from their services. But due to the way requests are delegated (via the HTTP GET verb), certain limitations restrict you from doing more than retrieving simple data. Second, you know that in the server-side code (in the code-behind files), you have full access to other domains through the rich APIs afforded to you in .NET. This leads to the second option for making cross-domain calls: calls through the server.
To better understand this scenario, let’s revisit the first Web Service call you made at the beginning of the chapter. You called a GetLocationCount method to retrieve the number of stores in a ZIP code. Let’s pretend that another Web Service on a different domain aggregates this total. You can’t call that service directly from JavaScript, but you can instead leverage the local Web Service as a proxy to the outside service.
You can think of the local service as the middle-man in a transaction. As the client, you don’t know much about the external service, nor should you. All you know is that you’re interested in the data it provides. The local service handles all the plumbing and the complicated work of communicating with the remote server. Figure 5.8 illustrates the concept of using a local server as a proxy for calling the remote APIs of a remote server.
This is the recommended way of communicating with remote servers. Let’s build an example that demonstrates how it works.
In this section, you’ll put together an application that retrieves and works with remote services like the Microsoft Virtual Earth map and the Yahoo! Geocoding APIs. This practice of pulling together content and data from other remote services into a single application is sometimes referred to as a mashup. Figure 5.9 shows the mashup application in action.
Mashup (or mash it up) is a Jamaican Creole term meaning “to destroy.” In the context of reggae or ska music, it can take on a positive connotation and mean an exceptional performance or event. The term has also been used in hip-hop, especially in cities such as New York that have a large Jamaican population. In popular culture, a mashup can mean a musical genre of songs that consists entirely of parts of other songs, or a website or web application hybrid that combines content from more than one source into an integrated experience.
The first step in putting together this application is to create a local Web Service that you can use to communicate with other remote services. Because the Yahoo! APIs are on another domain, you’ll use this service to facilitate any requests to the APIs for data. Listing 5.15 shows the contents of the local service called GeocodeService.
<%@ WebService Language="C#" Class="AspNetAjaxInAction.GeocodeService" %> using System; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; using System.Web.Script.Services; namespace AspNetAjaxInAction { [ScriptService] [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class GeocodeService : System.Web.Services.WebService { [WebMethod] public Location GetLocationData(string street, string zip, string city, string state, string country) { return YahooProvider.GetLocationData(street, zip, city, state, country); } } }
The service has just one method: GetLocationData. The method returns an instance of the Location class that you also define in the project. This class provides a simple structure for defining the geographical coordinates of a location on a map. Because you decorated the GeocodeService class with the ScriptService attribute, the methods and types (such as Location; see listing 5.16) that it interacts with are generated in the client proxies.
using System; namespace AspNetAjaxInAction { public class Location { private double latitude = 0.0; private double longitude = 0.0; public double Latitude { get { return this.latitude; } set { this.latitude = value; } } public double Longitude { get { return this.longitude; } set { this.longitude = value; } } public Location() { } } }
The last server-side class, an implementation class called YahooProvider, is used to encapsulate all the details of communication with the Yahoo! service. Listing 5.17 contains the implementation for the provider class.
using System; using System.Web; using System.Net; using System.Xml; using System.Globalization; namespace AspNetAjaxInAction { public class YahooProvider { private readonly static string apiKey = "YD-xeRMEJo_JX3b3GYHNVfy7N0Xow0Uuko"; private readonly static string geocodeUriFormat = "http://local.yahooapis.com/MapsService/V1/geocode?appid={0}-&street={1}&zip={2}&city={3}&state={4}"; public static Location GetLocationData(string street, string zip, string city, string state, string country) { // Use an invariant culture for formatting numbers. NumberFormatInfo numberFormat = new NumberFormatInfo(); Location loc = new Location(); XmlTextReader xmlReader = null; try { HttpWebRequest webRequest = GetWebRequest(street, zip, city, state); HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse(); using (xmlReader = new XmlTextReader(response.GetResponseStream())) { while (xmlReader.Read()) { if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.Name == "Result") { XmlReader resultReader = xmlReader.ReadSubtree(); while (resultReader.Read()) { if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.Name == "Latitude") { loc.Latitude = Convert.ToDouble(xmlReader.ReadInnerXml(), numberFormat); } if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.Name == "Longitude") { loc.Longitude = Convert.ToDouble(xmlReader.ReadInnerXml(), numberFormat); break; } } } } } finally { if (xmlReader != null) xmlReader.Close(); } // Return the location data. return loc; } private static HttpWebRequest GetWebRequest(string street, string zip, string city, string state) { string formattedUri = String.Format(geocodeUriFormat, apiKey, street, zip, city, state); Uri serviceUri = new Uri(formattedUri, UriKind.Absolute); return (HttpWebRequest)System.Net.WebRequest.Create(serviceUri); } } }
We won’t go over the details of the YahooProvider class because the purpose of this example is to demonstrate how cross-domain calls can be initiated from the client. However, we include all the code to ensure that you can follow along and successfully perform a cross-domain request just as you will here.
We decided to keep the logic for making the remote call separate from the local web service definition because the YahooProvider class is representative of the server class used to create and send the request to the remote web service. Nothing prevents you from, for example, using the server class generated using the WSDL of a remote web service instead of manually coding a specific provider.
All the pieces are in place on the server side for reaching out to a remote service. Now, you need to put the pieces together on the client to make this a true mashup.
Now that all the needed server classes are in place, you can kick off the cross-domain call using JavaScript from the browser. Listing 5.18 shows the client-side code for the geographical mashup. Add this code in the form tag of a web page in the ASP.NET AJAX enabled website.
First, included in the ScriptManager is the external script reference for the Virtual Earth map. It downloads the functionality you need to display and update the map on the page. Next in the ScriptManager declaration is a reference to the local service. This is the entry point into the other domains that are otherwise inaccessible from the client.
In the markup portion of the page is a simple div element, which is set aside to host the Virtual Earth map. Finally, in the script, when the pageLoad event is fired by the Application object (see chapter 2), you create the Virtual Earth map. Then, you make a call to the local service that returns the geographical coordinates for a location on the map. In the callback routine, you update the map with a new pushpin for the location.
This mashup demonstrates how to make cross-domain calls in a safe manner. It also shows how you can import functionality from external scripts to add additional resources to a page. Although this approach seems like the most logical one for communicating with remote services, there is another option we’ve yet to cover: bridges.
The ASP.NET Futures CTP offers another alternative for communicating with remote services. This approach, or technology, is suitably referred to as bridges or the bridge technology. Developers who leverage bridges can create gateways to remote services both programmatically and declaratively with just a few lines of code. Because the bridge code runs in the scope of the web application, the browser can communicate exclusively with the local server. On the server side, the bridge can create a dialogue with external services and subsequently return data to the browser.
The bridge technology was almost removed from the ASP.NET AJAX framework shortly before it was released. Due to customer demand, it made its way back into the Futures CTP. At the time of this writing, the bridge technology suffers from a number of symptoms that make it complex and challenging to implement.
As we mentioned, bridges require the ASP.NET Futures CTP, which means the Microsoft.Web.Preview.dll assembly must be added as a reference to the website. The final step in configuring bridges is to update the site’s web.config file. Beginning with the build provider, you must add a new entry to the <compilation> section to ensure that a server-side class is generated for a bridge file:
<compilation debug="true"> <assemblies> ... </assemblies> <buildProviders> <add extension=".asbx" type="Microsoft.Web.Preview.Services.BridgeBuildProvider" /> </buildProviders> </compilation>
Finally, you need to include an entry for the HTTP handler that will manage the requests to the .asbx files:
<httpHandlers> ... <add verb="GET,HEAD,POST" path="*.asbx" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="false"/> </httpHandlers>
You’re now ready to begin experimenting with the bridge technology. Let’s put together an example that communicates with a well-known site, Flickr, to query and display photos from users around the globe.
Flickr offers a wealth of options for how you can format requests and consequently retrieve responses. Some of the request formats include REST, XML-RPC, and SOAP. Response formats consist of REST, PHP, JSON, SOAP, and others. You can find a summary of these services and formats, and a good starting point for learning about Flickr services, at http://www.flickr.com/services/api/.
For this example, you’re interested in performing a search against the photos on the site. To keep things simple, you’ll use the REST option, which is available at http://api.flickr.com/services/rest/. Remember, REST services typically provide a flexible interface that lets you append details about the request to the URL. For instance, a search for photos tagged with the keyword ajax might look like this:
http://api.flickr.com/services/rest/?method= flickr.photos.search&api_key= 5cbeb1d1a24ac4698a51f0762ee28c0c&tags=ajax&extras=tags
Notice how parameters for the search criteria, such as tags=ajax, and other important elements are passed along in the URL of the request. On the provider’s end, the parameters are parsed from the request and then used to perform the search, resulting in a collection of photos that are returned in an XML format (see figure 5.10).
We’ll discuss how to decode this response shortly, but first let’s put together the pieces that make the request by examining the bridge file that makes this possible.
Earlier, we mentioned a new file extension designated for bridges. This .asbx extension is basically an XML file that serves as a roadmap to an external service and its methods. Listing 5.19 provides an example of how a request to the Flickr REST service is mapped out.
The roadmap begins with the bridge tag and the declaration of a namespace and class name for the service. You can relate this to the declaration of a local Web Service and how it’s perceived by the client. In this tag are the proxy element and the declaration of the type of proxy you want to leverage: Microsoft.Web.Preview.Services.BridgeRestProxy. This type can also be a custom class from the App_Code folder of the website or one of a few other options. Also in the proxy tag is the URL to the REST service you’ll be communicating with.
Subsequently, a collection of methods is exposed to the client. You can liken this to how web methods are declared in a service. The method tag declares the name of the method you can call from JavaScript. In this tag are the input tag and a collection of parameters that define the fields you’ll pass into the request, ultimately forming the URL that is sent to the service.
The parameters were determined by examining the online documentation for the flickr.photos.search API (see http://www.flickr.com/services/api/flickr.photos.search.html). We chose a few of the optional ones to conserve space and maintain the focus on the bridge technology.
Now that you have the bridge file configured, you can move ahead with calling it from the JavaScript. In order for this to happen, you must first add a service reference to the ScriptManager on the page:
<asp:ScriptManager ID="ScriptManager1" runat="server"> <Services> <asp:ServiceReference Path="flickr.asbx" InlineScript="true" /> </Services> </asp:ScriptManager>
Just like a reference to a local Web Service, this action provides you with a client-side proxy to the bridge file. The call to perform a search can now be executed as easily as a call to a local service:
AspNetAjaxInAction.FlickrSearch.Search({tags:"Microsoft"}, onSearchComplete, onSearchFailed);
The first parameter of the call takes a collection of input parameters. In the .asbx file, you hard-coded all the parameters except one: tags. The next set of parameters follows the familiar pattern of making network calls—callback functions for success and failure, and the optional user-context parameter.
From the perspective of the client-side code, nothing has changed. But this time, the information returned isn’t as friendly. This brings us to another feature of bridges: the ability to transform the data coming back into something more usable or presentable.
The bridge file supports the ability to transform or convert the response from the service before it reaches the caller. A few built-in transformers make this possible, including one that maps elements and tags in the response to an object. Listing 5.20 illustrates how adding a transformer to the bridge file can alter the response you get in the browser from XML to an object that’s friendlier and easier to work with.
You use the XPathBridgeTransformer to query the results and build a custom object. Another transformer that is available is the XsltBridgeTransformer, which lets you apply an XSLT style sheet to the response. Using XPath queries, you create a custom object by assigning the nodes in the response to a set of properties.
Now, from the newly formatted response, you can work with the array of objects returned to format the page with something more appealing to the user. Listing 5.21 shows the markup and client-side script used to provide the user with a visually meaningful set of results.
<div> <img src="/images/flickr_logo_gamma.gif.v1.5.7" width="98" height="26" /> <input id="flickrSearch" type="text" /> <input id="search" type="button" value="Search" onclick="doSearch();" /> <span id="searching" style="display: none;"> <img src="images/indicator.gif" /> Searching... </span> <div id="summary"></div><hr /> <span id="photoList"></span> </div> <script type="text/javascript" language="javascript"> function doSearch(){ var keywords = $get("flickrSearch").value; $get("searching").style.display = "inline"; AspNetAjaxInAction.FlickrSearch.Search( {tags:keywords}, onSearchComplete, onSearchFailed); } function onSearchComplete(results){ $get("searching").style.display = "none"; $get("summary").innerHTML = formatSummary(results, $get("flickrSearch").value); var photos = new Sys.StringBuilder(); photos.append("<table>"); for (var i = 0; i < results.length; i++){ var photo = results[i]; photos.append("<tr>"); photos.append(formatImage(photo)); photos.append(formatDetails(photo)); photos.append("<tr>"); } photos.append("</table>"); $get("photoList").innerHTML = photos.toString(); } function onSearchFailed(error){ $get("searching").style.display = "none"; alert(error.get_message()); } function formatSummary(photos, tags){ var summary = new Sys.StringBuilder(); summary.append(photos.length); summary.append(" results found for photos tagged with "); summary.append("<b>" + tags + "</b>" + "."); return summary.toString(); } function formatDetails(photo){ var details = new Sys.StringBuilder(); details.append("<td>"); details.append("<div>"); details.append(photo.Title); details.append("</div>"); details.append("<div>"); details.append("Tags: " + photo.Tags); details.append("</div>"); details.append("</td>"); return details.toString(); } function formatImage(photo){ var link = new Sys.StringBuilder(); link.append("<td>"); link.append("<img src='http://farm"); link.append(photo.FarmID); link.append(".static.flickr.com/"); link.append(photo.ServerID); link.append("/" + photo.ID + "_"); link.append(photo.Secret); link.append("_s.jpg'"); link.append(" />"); link.append("</td>"); return link.toString(); } </script>
Now for the grand finale. When you execute the application and perform a search for microsoft surface, you get a result similar to that shown in figure 5.11.
Bridges offer another alternative for communicating with external services from JavaScript. You have to be aware of a few gotchas when working with them: They’re hard to debug, they lack support in Visual Studio (no IntelliSense, debugging, or tracing), and they’re still under development. For these reasons, we feel you should be cautious when working with bridges and give strong consideration to whether there is sufficient benefit to using them instead of calling a local Web Service.
In the next section, we’ll dive into ASP.NET AJAX’s support for some of the application services in ASP.NET—notably authentication, profile, and roles.
ASP.NET 2.0 includes a rich set of application services. Their purpose is to serve as building blocks for common activities on a site. They significantly increase productivity and save time for developers who routinely perform these general actions (the ambitions of every framework). In this section, we’ll examine how you can invoke these services from the client-side script.
The first release of the ASP.NET AJAX framework supports two services: authentication and profile. The next version (which will ship with Orcas, the next version of Visual Studio) will include roles and perhaps a few more services as well. We’ll begin this section by focusing on the services supported in the 1.0 version of the framework. Subsequently, we’ll provide a preview of the roles service offered in Orcas.
Enabling these services requires a few updates to your web.config file. First, each service must be added to the sectionGroup section under the configuration group:
<sectionGroup name="webServices" ... <section name="profileService" type="System.Web.Configuration.ScriptingProfileServiceSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/> <section name="authenticationService" type="System.Web.Configuration. ScriptingAuthenticationServiceSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>
Next, in the webServices collection of the system.web.extensions group, you must enable each service:
<system.web.extensions> <scripting> <webServices> <authenticationService enabled="true" requireSSL = "false"/> <profileService enabled="true" /> </webServices> </scripting> </system.web.extensions>
The last step is the configuration of the data store—the location where membership and profile information is stored. Out of the box, these application services use the SQL-Express provider. For this example and the others in the book, we’ll stick with this option.
If you’d like to use the full version of SQL 2000 or 2005, or you’re interested in how to configure the database for application services, go here: http://msdn2.microsoft.com/en-us/library/aa479307.aspx.
With the configuration complete, you’re ready to work with these services. Let’s begin with the most common one: authentication.
Authentication is the means by which a user is identified with a set of credentials to validate. ASP.NET AJAX currently supports only forms authentication. This is a cookie-based authentication scheme where a user’s credentials (username and password) are stored in a database or file. To configure this type of validation, you must update web.config as follows:
<authentication mode="Forms"> <forms cookieless="UseCookies" loginUrl="~/Login.aspx" /> </authentication>
Notice how you include and initialize the loginUrl property. Users who attempt to view pages of the site that they aren’t authorized to access will be redirected to this page. With this in mind, create a new folder for the site called Secure, and add to it its own web.config with the following settings:
<?xml version="1.0"?> <configuration> <system.web> <authorization> <deny users="?"/> <allow users="*"/> </authorization> </system.web> </configuration>
In this setup, you grant access to authenticated users and reject anonymous ones (for additional information about authorization support, see http://msdn2.microsoft.com/en-us/library/8d82143t(VS.71).aspx). In the secure folder, add another file called ContactInformation.aspx, which will ultimately allow authenticated users to update their home address information.
Now, if you attempt to reach the new page in the Secure folder, you’re redirected to the login page as expected. Here, you provide the user with a form for entering in their credentials. Figure 5.12 shows the login page.
To authenticate the user, you have to read the values from the form and call the authentication service for validation. Listing 5.22 shows the client-side script that makes this possible.
When the page loads, you set the default callback function for the service’s login method. This isn’t a requirement, and it can be overwritten when you call the function—this is just another approach for setting the callback. Next, the loginUser function is called when the Login button on the form is clicked. You retrieve the user credentials and then call the login method to validate them. If the call succeeds, regardless of whether the credentials are valid, the callback function onLoginCompleted is called. Passed into the function’s first parameter is a Boolean value that indicates if the credentials are valid. Because you specified a URL in the fifth parameter, the user is redirected to that address if validation is successful. If something goes wrong during validation, such as an error on the server, then the failed callback function is called.
We briefly mentioned the login method used to validate the user credentials. Listing 5.23 shows the structure of the method along with brief comments for each parameter.
You’re calling the authentication service, but where exactly is it? The profile and authentication services are built into the framework. In the case of the authentication service, a class called Sys.Services._AuthenticationService contains the implementation for the service. The leading underscore indicates that the class is treated like a private member (there are no real private classes in JavaScript). You interact with this class through the Sys.Services.AuthenticationService variable. This abstraction between the object and the actual implementation class provides a singleton-like access point to the service.
public Object login(Object username, // username to validate Object password, // password to validate Object isPersistent, // remember me? Object customInfo, // reserved for future Object redirectUrl, // redirect on auth Object loginCompletedCallback, // call on success Object failedCallback, // call on failure Object userContext); // user context
This gives you a brief introduction to how the authentication service can be called from JavaScript. Now that you’re validating a user and redirecting them to a secure (authenticated users only) page, we can look at another service that adds interaction and personalization for users: profile.
In ASP.NET 2.0, the profile provider stores and retrieves information about a site’s users, much like session. However, with profile, because the information is saved to a database, the data is persisted and can be retrieved and configured at any time. This differs from session, where information is erased once the user logs off the site or their session expires. Adding properties to a user profile is as easy as updating web.config with a few extra lines, as shown in listing 5.24.
<profile enabled="true"> <properties> <add name="Address1" /> <add name="Address2" /> <add name="City" /> <add name="State" /> <add name="Zip" /> </properties> </profile>
In this example, you add properties that relate to an individual’s home address. By default, each property is of type string. The goal in this section will be to read from and update this profile information from the browser in a seamless and non-intrusive manner.
The contact page we discussed earlier is the perfect candidate for integrating with the profile service. Here, you’ll provide a form for the user to update and read their information from their profile, all without the cost of a postback. Figure 5.13 shows the contact form when it’s first loaded.
At the top of the page is a link for logging out. When clicked, it logs out the authenticated user and directs them back to the login page:
function logoutUser(){ Sys.Services.AuthenticationService.logout(null, onLogoutCompleted, onLogoutFailed, null); }
The logout function of the authentication service is straightforward. The first parameter optionally takes in the URL for redirecting the user on success. If null is passed, the loginUrl specified in the web.config is used. The callback pattern for success and failure continues with the second and third parameters. The user context is available in the last parameter.
To read from a user’s profile, you call the service’s load function and populate the elements on the page with the results; see listing 5.25.
Because you pass in null as the first parameter, you retrieve all the profile properties that you defined in web.config. Then, when the successful callback function is called, you can read the properties and populate the form accordingly.
Updating the user’s profile properties is just as easy—all you have to do is initialize the values and then call the save function in the service. See listing 5.26.
function saveProfile(){ var addr1 = $get("address1").value; var addr2 = $get("address2").value; var city = $get("city").value; var state = $get("state").value; var zip = $get("zip").value; Sys.Services.ProfileService.properties.Address1 = addr1; Sys.Services.ProfileService.properties.Address2 = addr2; Sys.Services.ProfileService.properties.City = city; Sys.Services.ProfileService.properties.State = state; Sys.Services.ProfileService.properties.Zip = zip; Sys.Services.ProfileService.save(null, onSaveCompleted, onSaveFailed, null); } function onSaveCompleted(numProperties, userContext, methodName){ $get("updating").style.display = "none"; } function onSaveFailed(error, useContext, methodName){ alert(error.get_message()); }
Again, you follow the same pattern of calling the service, passing in the name of callback functions for success or failure, and updating the UI on return. Let’s take this a step further by implementing a feature that automatically saves a user’s profile as they edit their information.
The simplicity and nature of this service provide you with a great opportunity: the ability to save user profile settings automatically. With the use of a timer, you can periodically save profile information. This added value will be appreciated by users who lose their Internet connection intermittently or forget to click the Save button on the form (it does happen).
To integrate this useful pattern, you kick off the timer after the profile settings are originally loaded. Then, when the interval for the timer elapses, you call the same saveProfile function you used earlier:
function onLoadCompleted(numProperties, userContext, methodName){ ... window.setInterval(tryAutoSave, 10000); } function tryAutoSave(){ saveProfile(); }
This is a nice feature that doesn’t require a lot of coding. This leads us to the last service, roles, which grants you the ability to inquire about the roles a user has been assigned.
The next version of the ASP.NET AJAX framework will include another built-in application service: roles. The simplest way to demonstrate how to use the role service is to designate a portion of the page that only certain users in a role can view.
The built-in roles service isn’t part of the 1.0 release of ASP.NET AJAX. This section provides a sneak peek at what is to come in the next version. This version will be baked into the next release of Visual Studio, codenamed Orcas. At the time of this writing, downloads for Orcas are available here: http://msdn2.microsoft.com/en-us/vstudio/aa700831.aspx.
Before we begin, update the web.config file for this service by adding another section for the service under the sectionGroup area:
<sectionGroup name="webServices"... ... <section name="roleService" type="System.Web.Configuration.ScriptingRoleServiceSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>
Next, you need to enable the service along with the others:
<system.web.extensions> <scripting> <webServices> ... <roleService enabled="true" /> </webServices> </scripting> </system.web.extensions>
That’s all you need for the configuration portion. For the UI, add a div element named adminView to the contact information page. By default, you hide this element by setting its display style to none:
<div id="adminView" style="display: none;"> <b>Only admins can see this message!</b> </div>
After the page loads, you can retrieve the current user’s roles and make the adminView element visible if they’re in the designated role:
The first task is to call the load function in the service. When it returns successfully, you check to see if the user is in the Admin role by calling the isUserInRole function. An alternative would be to retrieve a comma-delimited list of roles from the roles property:
Sys.Services.RoleService.get_roles();
This requires you to then split and parse the list—which for a different scenario might be more efficient than calling isUserInRole repeatedly. Because you’re only comparing against a single role, the original approach makes more sense.
Working with the ASP.NET application services is simple. Because they’re nothing more than built-in Web Services, the same patterns we covered earlier—callbacks, user context, timeout, and so on—apply here as well.
We’ve created a sample message board application that combines all the topics discussed in this chapter, along with some content from chapter 9. The source code for the message board application is available on the book’s website at http://www.manning.com/gallo. We mention it here as a reminder that you can download the code from the site for additional examples. Figure 5.14 shows the application in action.
In this essential chapter, we covered the most primitive and influential pattern used in Ajax programming: making asynchronous calls from the browser to the server. The plumbing work needed to accomplish this task, along with the abstraction of cross-browser discrepancies, are handled for you by the ASP.NET AJAX framework.
With the help of Web Service proxies generated by the framework, you can use local Web Services as a business layer to the applications that run on the browser. You can also use local Web Services to facilitate communication with other remote servers and services on the web. This chapter covered the most typical and recommended approach for building Ajax applications.
Although this chapter was dedicated solely to client-centric programming with ASP.NET AJAX, the next few chapters return to the server-side element of the framework. To become a solid ASP.NET AJAX developer, you must be comfortable with both realms of the architecture. In the next chapter, we’ll revisit the UpdatePanel control and take a thorough walk through its features.
3.144.110.253