Extreme ASP.NET

Client-Side Web Service Calls with AJAX Extensions

Fritz Onion

Code download available at:  Extreme ASP NET 2007_01.exe(160 KB)

Contents

Calling Web Services with AJAX
How It Works
Serialization
Conclusion

Since its inception, ASP.NET has fundamentally been a server-side technology. There were certainly places where ASP.NET would generate client-side JavaScript, most notably in the validation controls and more recently with the Web Part infrastructure, but it was rarely more than a simple translation of server-side properties into client-side behavior-you as the developer didn't have to think about interacting with the client until you received the next POST request. Developers needing to build more interactive pages with client-side JavaScript and DHTML were left to do it on their own, with some help from the ASP.NET 2.0 script callbacks feature. This has changed completely in the last year.

At the Microsoft Professional Developer's Conference in September 2005, Microsoft unveiled a new add-on to ASP.NET, code-named "Atlas," which was focused entirely on leveraging client-side JavaScript, DHTML, and the XMLHttpRequest object. The goal was to aid developers in creating more interactive AJAX-enabled Web applications. This framework, which has since been renamed with the official titles of Microsoft AJAX Library and the ASP.NET 2.0 AJAX Extensions, provides a number of compelling features ranging from client-side data binding to DHTML animations and behaviors to sophisticated interception of client POST backs using an UpdatePanel. Underlying many of these features is the ability to retrieve data from the server asynchronously in a form that is easy to parse and interact with from client-side JavaScript calls. The topic for this month's column is this new and incredibly useful ability to call server-side Web services from client-side JavaScript in an ASP.NET 2.0 AJAX Extensions-enabled page.

Calling Web Services with AJAX

If you have ever consumed a Web service in the Microsoft .NET Framework, either by creating a proxy using the wsel.exe utility or by using the Add Web Reference feature of Visual Studio, you are accustomed to working with .NET types to call Web services. In fact, invoking a Web service method through a .NET proxy is exactly like calling methods on any other class. The proxy takes care of preparing the XML based on the parameters you pass, and it carefully translates the XML response it receives into the .NET type specified by the proxy method. The ease with which developers can use the .NET Framework to consume Web service endpoints is incredibly enabling, and is one of the pillars that make service-oriented applications feasible today.

The ASP.NET 2.0 AJAX Extensions enable this exact same experience of seamless proxy generation for Web services for client-side JavaScript that will run in the browser. You can author an .asmx file hosted on your server and make calls to methods on that service through a client-side JavaScript class. For example, Figure 1 shows a simple .asmx service that implements a faux stock quote retrieval (with random data).

Figure 1 StockQuoteService.asmx

<%@ WebService Language="C#"
               Class="MsdnMagazine.StockQuoteService" %>

using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
// From Microsoft.Web.Extensions.dll assembly
using Microsoft.Web.Script.Services; 

namespace MsdnMagazine
{
    [WebService(Namespace = "https://msdnmagazine.com/ws")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ScriptService]
    public class StockQuoteService : WebService
    {
        static Random _rand = new Random(Environment.TickCount);

        [WebMethod]
        public int GetStockQuote(string symbol)
        {
            return _rand.Next(0, 120);
        }
    }
}

In addition to the standard .asmx Web service attributes, this service is adorned with the ScriptService attribute that makes it available to JavaScript clients as well. If this .asmx file is deployed in an ASP.NET AJAX-Enabled Web application, you can invoke methods of the service from JavaScript by adding a ServiceReference to the ScriptManager control in your .aspx file (this control is added automatically to your default.aspx page when you create a Web site in Visual Studio using the ASP.NET AJAX-enabled Web site template):

<asp:ScriptManager ID="_scriptManager" runat="server">
  <Services>
    <asp:ServiceReference Path="StockQuoteService.asmx" />
  </Services>
</asp:ScriptManager>

Now from any client-side JavaScript routine, you can use the MsdnMagazine.StockQuoteService class to call any methods on the service. Because the underlying mechanism for invocation is intrinsically asynchronous, there are no synchronous methods available. Instead, each proxy method takes one extra parameter (beyond the standard input parameters)- a reference to another client-side JavaScript function that will be called asynchronously when the method completes. The example page shown in Figure 2 uses client-side JavaScript to print the result of calling the stock quote Web service to a label (span) on the page.

Figure 2 Sample Web Service Client Page

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="https://www.w3.org/1999/xhtml" >
<head runat="server">
 <title>ASP.NET AJAX Web Services: Web Service Sample Page</title>

 <script type="text/javascript"> 
    function OnLookup()
    {           
      var stb = document.getElementById("_symbolTextBox");  
      MsdnMagazine.StockQuoteService.GetStockQuote(
        stb.value, OnLookupComplete);
    }
    
    function OnLookupComplete(result)
    {
      var res = document.getElementById("_resultLabel");
      res.innerHTML = "<b>" + result + "</b>";
    }
  </script>    
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="_scriptManager" runat="server">
      <Services>
        <asp:ServiceReference 
             Path="services/StockQuoteService.asmx" />
      </Services>
    </asp:ScriptManager>
    <div>
    <h1>ASP.NET AJAX Web Services: Web Service Sample Page</h1>
    Enter symbol: 
    <asp:TextBox runat="server" id="_symbolTextBox" />
    <br />
    <input onclick="OnLookup();" id="_lookupButton" type="button" 
           value="Lookup" />
    <br />
    <asp:Label runat="server" id="_resultLabel" />
    </div>
    </form>
</body>
</html>

If something goes wrong with a client-side Web service call, you definitely want to let the client know, so it's usually wise to pass in another method that can be invoked if an error, abort, or timeout occurs. For example, you might change the OnLookup method shown previously as follows, and add an additional OnError method to display any problems:

function OnLookup()
{           
  var stb = document.getElementById("_symbolTextBox");  
  MsdnMagazine.StockQuoteService.GetStockQuote(
    stb.value, OnLookupComplete, OnError);
}
    
function OnError(result)
{
  alert("Error: " + result.get_message());
}

This way if the Web service call fails, you will notify the client with an alert box. You can also include a userContext parameter with any Web service calls made from the client, which is an arbitrary string passed in as the last parameter to the Web method, and it will be propagated to both the success and failure methods as an additional parameter. In this case, it might make sense to pass the actual symbol of the stock requested as the userContext so you can display it in the OnLookupComplete method:

function OnLookup()
{           
  var stb = document.getElementById("_symbolTextBox");  
  MsdnMagazine.StockQuoteService.GetStockQuote(
    stb.value, OnLookupComplete, OnError, stb.value);
}

function OnLookupComplete(result, userContext)
{
  // userContext contains symbol passed into method
  var res = document.getElementById("_resultLabel");
  res.innerHTML = userContext + " : <b>" + result + "</b>";
}

If you find that you're making many different calls to a Web service, and that you re-use the same error and/or complete methods for each call, you can also set the default error and succeeded callback method globally. This avoids having to specify the pair of callback methods each time you make a call (although you can choose to override the globally defined methods on a per-method basis). Here is a sample of the OnLookup method that sets the default succeeded and failed callback methods globally instead of on a per-call basis.

// Set default callbacks for stock quote service
MsdnMagazine.StockQuoteService.set_defaultSucceededCallback(
                 OnLookupComplete);
MsdnMagazine.StockQuoteService.set_defaultFailedCallback(
                 OnError);
function OnLookup()
{           
  MsdnMagazine.StockQuoteService.GetStockQuote(stb.value);
}

Another interesting alternative to building a complete .asmx file for your Web service methods is to embed the Web service methods directly in the page class. If it doesn't make sense to build a complete Web service endpoint for the methods you want to call, you can expose a Web method from your page that is callable from client-side JavaScript by adding a server-side method to your page (either directly in the page or in the codebehind) and annotating it with the WebMethod attribute. You will then be able to invoke it via the client-side object PageMethods. The example in Figure 3 shows the stock quote service sample rewritten to be entirely contained in a single page instead of split into a separate Web service.

Figure 3 A Single-Page Stock Quote Service

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="https://www.w3.org/1999/xhtml" >
<head runat="server">
<title>ASP.NET AJAX Web Services: Web Service Sample Page</title>
  <script runat="server">
        private static Random _rand = new Random();
        
        [WebMethod]
        public static float GetStockQuoteFromPage(string symbol)
        {
            return _rand.Next(0, 120);
        }
  </script>

  <script type="text/javascript">    
  function OnLookup()
  {           
    var stb = document.getElementById("_symbolTextBox");       
    PageMethods.GetStockQuoteFromPage(stb.value,
                  OnLookupComplete);
  }
    
  function OnLookupComplete(result)
  {
    var res = document.getElementById("_resultLabel");
    res.innerHTML = "<b>" + result + "</b>";
  }
  </script>  
</head>
<body>
    <form id="form1" runat="server">
    <div>    
    <h1> ASP.NET AJAX Web Services: Web Service Sample Page </h1>
     Enter symbol: 
        <asp:TextBox runat="server" id="_symbolTextBox" />
        <br />
        <input onclick="OnLookup();" id="_lookupButton" 
               type="button" value="Lookup" />
        <br />
        <asp:Label runat="server" id="_resultLabel" />
    </div>
    </form>
</body>
</html>

Bear in mind that these client-side proxies can only be generated from ASP.NET .asmx endpoints, Windows Communication Foundation .svc endpoints, or WebMethods embedded directly in a page, and are not a general mechanism for calling arbitrary Web services. In fact, there is a general restriction on the underlying XmlHTTPRequest object that requests be restricted to the same domain from which the page was loaded (for security reasons), so this technique could not be used to call arbitrary Web services regardless of whether the client-side proxies supported it. If you do find the need to call external Web services, your best bet is to set up a bridge .asmx endpoint in your own application that calls into a .NET proxy class (generated with wsdl.exe or Add Web Reference in Visual Studio) for the external Web service.

How It Works

It might seem surprising at first that you can take a standard .asmx Web service and access it from client-side JavaScript within a browser with almost no changes. The secret lies in the registration of a new .asmx HTTP handler, added to the configuration file of every ASP.NET AJAX-enabled Web site:

<httpHandlers>
  <remove verb="*" path="*.asmx"/>
  <add verb="*" path="*.asmx" 
       type="Microsoft.Web.Services.ScriptHandlerFactory" 
       validate="false"/>
</httpHandlers>

This newly registered handler will invoke the standard Web service handler (System.Web.Services.Protocols.WebServiceHandlerFactory) if it is a standard Web service request made to an .asmx endpoint. If, however, the request has a trailing /js in the URL or contains a query string with an mn= variable (such as ?mn=GetStockQuote), the handler will either return a chunk of JavaScript that creates a client-side proxy for the Web service (the /js option), or it will invoke the corresponding method defined in the WebService-derived class and package up the response in a JavaScript Object Notation (JSON)-encoded string (the ?mn= option).

When a page includes a client-side reference to an .asmx service (via the ServiceReference element within the ScriptManager control), it injects a script element that references the .asmx file with a trailing /js, creating a proxy in the client. For example, the stock quote page I built earlier rendered with the following script element in it:

<script src="StockQuoteService.asmx/js" 
        type="text/javascript"></script>

This is, of course, in addition to the script references added for the Microsoft AJAX Libraries, which include the client-side features needed to interact with this proxy. If you try navigating to this endpoint yourself, you will see the following JavaScript (elided):

Type.registerNamespace('MsdnMagazine');
MsdnMagazine.StockQuoteService=function() {
  this._timeout = 0;
  this._userContext = null;
  this._succeeded = null;
  this._failed = null;
}
MsdnMagazine.StockQuoteService.prototype={
GetStockQuote:Sys.Net._WebMethod._createProxyMethod(this,
     "GetStockQuote", 
     "MsdnMagazine.StockQuoteService.GetStockQuote",
     "symbol"), ...
}

This JavaScript is using features of the Microsoft AJAX Libraries (like namespaces and the WebMethod class) that are included with every page that includes a ScriptManager control. The proxy method created by this JavaScript is initialized to invoke the .asmx endpoint with the query string ?mn=GetStockQuote in this case, so that whenever you call MsdnMagazine.StockQuoteService.GetStockQuote from the client, it turns into an asynchronous Web request for the same .asmx endpoint. This combination of client-side proxy generation and server-side support for JavaScript-initiated Web service calls means you can include client-side calls to your .asmx Web services in an intuitive way.

Serialization

The default serialization of AJAX-based Web services is JSON. If you look at a trace of the page shown in the last section in action, the bodies of the Web service request and response look like this:

Request: {"symbol":"ABC"}
Response: 51

This is definitely not the standard XML format you are probably accustomed to seeing when calling your .asmx Web services. Since .asmx endpoints were built to serialize into XML, one of the major additions of the ASP.NET 2.0 AJAX Extensions is a JSON serializer. There are actually two of them-one implemented in JavaScript for use on the client, and one implemented in .NET for use on the server, specifically when an AJAX client invokes an .asmx endpoint. The server-side serializer is available through the Microsoft.Web.Script.Serialization.JavaScriptSerializer class and the client-side serializer is available through Sys.Serialization.JavaScriptSerializer. One of the major advantages of using JSON as a serialization format over XML is that you can deserialize objects in JavaScript by simply evaluating a JSON string. The deserialize method of the client serializer class ends up being very short (error checking removed):

Sys.Serialization.JavaScriptSerializer.deserialize=
    function(){eval('('+data+')');}

The serialize method of the JavaScriptSerializer, on the other hand, is considerably more involved. Another advantage to using JSON is its relatively compact representation compared with the XML equivalent.

Much as the XmlSerializer used by standard Web services serializes types to XML, you can take almost any .NET type and serialize it into JSON with JavaScriptSerializer. If you want to try it yourself, it's just a matter of calling the Serialize method of the JavaScriptSerializer class. Figure 4 shows a sample console application that serializes the complex Person type (shown in Figure 5), as an example. (It must include an assembly reference to the Microsoft.Web.Extensions.dll installed in the Global Assembly Cache (GAC) with the ASP.NET AJAX Extensions.)

Figure 5 Sample Person Type

namespace MsdnMagazine.SampleTypes
{
    public class Person
    {
        private string _firstName;
        private string _lastName;
        private int _age;
        private bool _married;

        public bool Married
        {
            get { return _married; } set { _married = value; }
        }

        public int Age
        {
            get { return _age; } set { _age = value; }
        }

        public string FirstName
        {
            get { return _firstName; } set { _firstName = value; }
        }

        public string LastName
        {
            get { return _lastName; } set { _lastName = value; }
        }
    }
}

Figure 4 JSON Serialization Console Application

using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Web.Script.Serialization;

class App
{
    static void Main()
    {
        Person p = new Person();
        p.FirstName = "Bob";
        p.LastName = "Smith";
        p.Age = 33;
        p.Married = true;

        JavaScriptSerializer jss = new JavaScriptSerializer();
        string serializedPerson = jss.Serialize(p);
        Console.WriteLine(serializedPerson);
    }
}

The output of the console application would be the Person class in JSON format, or:

{"Married":true,"Age":33,"FirstName":"Bob","LastName":"Smith"}

Like XmlSerializer, JavaScriptSerializer will only serialize publicly accessible data in a type, and there is no support for resolving cyclic references. But any types that can be serialized by a standard .asmx Web service will work fine with this serializer (and yes, this includes DataSet). With this knowledge in place, you can build a more complex Web service, knowing that it will handle complex types as easily as any SOAP-based Web service defined in an .asmx file.

The example in Figure 6 shows a Web service called MarriageService that implements a Marry method to take two Person objects (as defined earlier) and modify their attributes appropriately. (The accompanying ASP.NET page is included with the code download for this issue.)

Figure 6 MarriageService.asmx

<%@ WebService Language="C#" 
    Class="MsdnMagazine.MarriageService" %>

using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using Microsoft.Web.Script.Services;
using MsdnMagazine.SampleTypes;

namespace MsdnMagazine
{
    [WebService(Namespace = "https://msdnmagazine.com/ws")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ScriptService]
    [GenerateScriptType(typeof(Person))]
    public class MarriageService : WebService
    {
        [WebMethod]
        public Person[] Marry(Person[] couple)
        {
          if (couple.Length != 2)
            throw new ArgumentException("2 persons only!");
          if (couple[0].Married || couple[1].Married)
            throw new ArgumentException("Tell your spouse first!");
            
          couple[0].LastName += "-" + couple[1].LastName;
          couple[1].LastName = couple[0].LastName;
          couple[0].Married = couple[1].Married = true;
          return couple;
        }
    }
}

If you'd rather work with XML in your client-side script, that option is available as well. In addition to the standard WebMethod attribute used when defining Web services, there is a new attribute in the Microsoft.Web.Script.Services namespace called ScriptMethod, which has a ResponseFormat property that can be set to either Json or Xml (it defaults to Json).

namespace PS
{
    [ScriptService] 
    [WebService(Namespace = "https://pluralsight.com/ws")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class StockQuoteService : WebService
    {
        [WebMethod]
        public int GetStockQuote(string symbol)
        {
            return (new Random()).Next(0, 120);
        }
    }
}

The serialized response would then be:

<?xml version="1.0" encoding="utf-8"?><int>74</int>

It is up to you when you invoke this method from client-side JavaScript to handle the XML response. If you plan on running a transformation on it or are already using MSXML, this may be useful.

Conclusion

It's worth pointing out that the ASP.NET 2.0 AJAX Extensions provide two pre-built services to tap into specific ASP.NET 2.0 application services from client-side code: ProfileService and AuthenicationService. With these two client-side proxy classes you can set and retrieve profile values for individual clients, as well as perform authentication (via the default Membership provider) and grant authentication cookies completely within client script.

While many discussions and demonstrations of the ASP.NET 2.0 AJAX Extensions focus on the flashy controls that enable responsive user interfaces, one of the most impressive and useful features is the ability to call Web services directly from client-side JavaScript. With a full .NET Framework/JSON serializer, direct integration with the familiar .asmx Web services, support for batching, and auto-generated bridges to external Web services, the breadth and depth of support for Web services makes this perhaps the most compelling feature of all.

Send your questions and comments for Fritz to  <xtrmasp@microsoft.com.>.

Fritz Onion is a cofounder of Pluralsight, a Microsoft .NET training provider, where he heads the Web development curriculum. Fritz is the author of Essential ASP.NET (Addison Wesley, 2003) and Essential ASP.NET 2.0 (Addison Wesley, 2006). Reach him at pluralsight.com/fritz.