- download sample code
>
Overview
The Microsoft ASP.NET AJAX platform, known previously as ATLAS and ASP.NET 2.0 AJAX Extensions and fully rolled into ASP.NET 3.5, offers rich functionality but in certain scenarios the the required .ASPX client page and ScriptManager control coupled with the Microsoft Ajax library may conflict with existing requirements or present unnecessary overhead.
This document will illustrate that it is possible to leverage the full power of the ASP.NET platform from client script with just a few lines of javascript code on an HTML page.
Caveats:
- This document will not attempt to illustrate a robust implementation of an AJAX client, only the barest minimum protocol, configuration and script for accessing ASP.NET endpoints from 'raw' JavaScript code. The implementation details documented herein can be applied to any JavaScript framework, such as jQuery, that has an AJAX implementation. Also, the author is currently developing a lightweight robust client script AJAX library that includes support for ASP.NET endpoints, including FormsAuthentication, to be introduced at a later date.
- This document will not attempt to address the very relevant issue of handling session state and ASP.net Authentication/Authorization from client script, only the mechanics of consuming the endpoints via XMLHttpRequest. These slightly more complicated issues will be addressed in a later document.
Asynchronous JavaScript And XML - AJAX
The simplest implementation of 'AJAX' involves instantiating an XMLHttpRequest object, overriding it's onreadystatechanged event to parse the response and then calling the send method.
Listing 1:
function createXHR()
{
var xhr;
if (window.XMLHttpRequest)
{
xhr = new XMLHttpRequest();
}
else if (window.ActiveXObject)
{
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
else
{
throw new Error("Could not create XMLHttpRequest object.");
}
return xhr;
}
var xhr = createXHR();
xhr.open("GET", "some web resource", true);
xhr.onreadystatechange = function()
{
if (xhr.readyState === 4)
{
var responseText = xhr.responseText;
var responseXML = xhr.responseXML;
}
};
xhr.send(null);
AJAX and ASP.net XML WebServices
Consuming an ASP.net XML WebService from client script is not much more involved than the basic implementation of AJAX and has been possible since 2.0 without the necessity of AJAX Extensions.
Any XML WebService can be consumed via a simple querystring GET or form POST provided all of the arguments are of a 'simple' or value type. You may have noticed this restriction when viewing the test page for a service you have created that contained an object as a method argument. This is where the ScriptService attribute and text/json content type come into play but we will get to that in the next section.
By default the 'HttpPostLocalhost' web service protocol is enabled which allows the generation of the test page when browsing the web service endpoint from the machine it is hosted on. This also allows any JavaScript running in the localhost domain to access the web service but fail upon deployment. This is probably not the desired result. An enabling of http protocols is required.
To make your XML service available to client script outside of the localhost domain you must enable the desired protocols in the 'system.web/webServices/protocols' configuration element.
Listing 2:
<system.web>
<webServices>
<protocols>
<add name="HttpGet"/>
<add name="HttpPost"/>
</protocols>
</webServices>
< ..... >
<system.web>
Given this configuration change, you may now call any XML webservice in your project from client script via simple GET and/or POST and recieve an XML response as long as the method arguments consist entirely of simple types.
Let our example WebService be as Listing 3:
Listing 3:
using System.Web.Script.Services;
using System.Web.Services;
namespace ClientScriptServices
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class WebService1 : WebService
{
[WebMethod]
public string Echo(string input)
{
return input;
}
[WebMethod]
public TestObj EchoTestObject(TestObj input)
{
return input;
}
}
public class TestObj
{
public int Age;
public string Name;
}
}
Listing 4:
var xhr = createXHR();
xhr.open("GET", "WebService1.asmx/Echo?input=Hello%20World!", true);
xhr.onreadystatechange = function()
{
if (xhr.readyState === 4)
{
?>\n<string xmlns="http://tempuri.org/">Hello World!</string>'
// xhr.responseXML is a valid DOMDocument of responseText
}
};
xhr.send(null);
Listing 5:
var xhr = createXHR();
xhr.open("POST", "WebService1.asmx/Echo", true);
xhr.onreadystatechange = function()
{
if (xhr.readyState === 4)
{
?>\n<string xmlns="http://tempuri.org/">Hello World!</string>'
// xhr.responseXML is a valid DOMDocument of responseText
}
};
xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
var postData = "input=Hello%20World!";
xhr.send(postData);
So, if your methods have simple arguments and XML is the desired result format, you have had everything you need to consume XML web services from client script since .net 2.0. While many use cases may fit these requirements/restrictions, I am quite sure that many many more use cases have either been scratched or had thier requirements mangled to fit. And lots of JavaScript XML parsing code written to interpret the results into a JSOB (JavaScript Object). You do what you gotta do.
If the service method arguments are of a complex type the use of a SOAP envelope become a necessity. There are examples of this on the web, though most are simply examples of munging a static string literal containing the XML of the SOAP envelope. In any case, this is no longer necessary and is beyond the scope of this document.
Enter the ScriptService attribute.
AJAX and ASP.net ScriptServices and static Page methods
The ScriptService attribute is usually associated, and explained, with registering a web service with a ScriptManager control and consumption of said service via dynamically generated script imported by the MS Ajax library. There may be scenarios in which the use of an .ASPX page as a client host and/or the introduction of the MS Ajax library on the client side is not possible, permitted and/or desired.
In these cases it is useful to know that the ScriptService attribute effectively adds another HttpHandler, named RestHandler, that is associated with the "application/json" content type that wraps the XML WebService with a JavaScriptSerializer. When a POST with content type "application/json" is made to a service that has the ScriptService attribute the method arguments are parsed and the output formatted with the JavaScriptSerializer class. How this is done and how we can further customize the runtime behavior to suit our needs will be covered in a later document. For the moment we will simply leverage the default behavior.
Listing 6:
var xhr = createXHR();
xhr.open("POST", "WebService1.asmx/Echo", true);
xhr.onreadystatechange = function()
{
if (xhr.readyState === 4)
{
}
};
xhr.setRequestHeader("content-type", "application/json");
var postData = '{"input": "Hello World!"}';
xhr.send(postData);
Listing 7:
var xhr = createXHR();
xhr.open("POST", "WebService1.asmx/EchoTestObject", true);
xhr.onreadystatechange = function()
{
if (xhr.readyState === 4)
{
}
};
xhr.setRequestHeader("content-type", "application/json");
var postData = '{input: { Name: "Foo", Age: 21 }}';
xhr.send(postData);
The responseText a valid JSON that can be evaluated, or parsed with json2.js for example, into a valid JSOB.
The RestHandler wraps all results in a 'd' object. This is, ostensibly, to help prevent cross site scripting attacks. Unless you wish to spread MS specific kludges throughout your client script, I suggest that you unwrap the result upon reciept.
When a method argument is an object, as in Listing 7, the postData JSON needs to be shaped as the CLR object, i.e. the JavaScriptSerializer must be able to parse it into an instance of the argument type. Nullable members may be omitted. JavaScript is liberal regarding the quoting of JSON atoms. JavaScriptSerializer is not so much. You must use double quotes as shown.
I would suggest becoming familiar with and using the defacto standard for JSON manipulation, Douglas Crockford's json2.js.
The section title mentions static Page methods. "PageMethods" are methods within an .aspx code file that are static and marked with the [WebMethod] attribute.
Let our sample WebForm (Page) be as Listing 8:
Listing 8:
using System;
using System.Web.Services;
using System.Web.UI;
namespace ClientScriptServices
{
public partial class WebForm1 : Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
[WebMethod]
public static string Echo(string input)
{
return input;
}
}
}
ASP.net AJAX also Page requests with the ScriptModule class that treats requests with a content type of "application/json" as ScriptService calls to the named PageMethod. You would typically make these methods callable by setting EnablePageMethods true on a ScriptManager control.
In the context of our scenario it is enough just to call them with code similar to Listings 6 and 7. Unlike when using ScriptManager, when using XMLHttpRequest, you may call PageMethods on any page in the current web application/site as if they were a ScriptService. The restriction that the method be static may somewhat limit the utility of this ability but it is listed here in the interest of thoroughness.
Listing 9:
var xhr = createXHR();
xhr.open("POST", "WebForm1.aspx/Echo", true);
xhr.onreadystatechange = function()
{
if (xhr.readyState === 4)
{
}
};
xhr.setRequestHeader("content-type", "application/json");
var postData = '{"input": "Hello World!"}';
xhr.send(postData);
AJAX and WCF Services
ASP.NET 3.5 introduced a new binding, webHttpBinding, that is particularly useful in our scenario. This new binding reduces the protocol for calling a WCF service from requiring a complex WCF SOAP envelope down to a simple ScriptService (RestHandler) call albeit with a different, more strict serializer.
Let our WCF service be as Listing 10:
Listing 10:
using System.ServiceModel;
using System.ServiceModel.Activation;
namespace ClientScriptServices
{
[ServiceContract]
public class Service1
{
[OperationContract]
public string Echo(string input)
{
return input;
}
[OperationContract]
public TestObj EchoTestObject(TestObj input)
{
return input;
}
}
}
We could have more easily added 'ServiceContract' and 'OperationContract' attributes to our web service class and exposed it as a WCF service as well as a web service. In the interest of clarity I have duplicated the code in a new service.
The default system.serviceModel configuration added by Visual Studio is similar to Listing 11.
Listing 11:
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="ClientScriptServices.Service1Behavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="ClientScriptServices.Service1Behavior" name="ClientScriptServices.Service1">
<endpoint address="" binding="wsHttpBinding" contract="ClientScriptServices.Service1">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
</system.serviceModel>
To leverate the webHttpBinding in the most basic way we need simply add a few configuration elements as shown in listing 12:
Listing 12:
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="AjaxEndpointBehavior">
<!-- required for JSON POST -->
<enableWebScript />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="ClientScriptServices.Service1Behavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="ClientScriptServices.Service1Behavior" name="ClientScriptServices.Service1">
<endpoint address="" binding="wsHttpBinding" contract="ClientScriptServices.Service1">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
<!-- required for JSON POST -->
<endpoint address="ajax" behaviorConfiguration="AjaxEndpointBehavior" binding="webHttpBinding" contract="ClientScriptServices.Service1" />
</service>
</services>
</system.serviceModel>
We have added 2 elements:
- an 'endpointBehaviors' element with a child element 'enableWebScript' to enable JSON POST calls to the service.
- an 'endpoint' element to the service element.
Notice the 'endpoint' attribute 'address' on both the default endpoint and our new AJAX endpoint. The default endpoint has an empty 'address' attribute. The result of this is that a call to 'Service1.svc/Echo' will be processed with the default endpoint configuration. Our new AJAX endpoint has an 'address' value of 'ajax'. This is an arbitrary identifier that allows multiple 'endpoint' configuration elements to handle the same service. The result of this is that to call Service1 from a script we would use the URL 'Service1.svc/ajax/Echo'. If the service was to be exposed ONLY to script we could delete the default endpoint element and omit the 'address' attribute in our new AJAX endpoint element.
For clarity we will pare down the 'system.serviceModel' configuration to the barest minimum required to consume a service only from script.
Listing 13:
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="AjaxEndpointBehavior">
<enableWebScript />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="ClientScriptServices.Service1Behavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="ClientScriptServices.Service1Behavior" name="ClientScriptServices.Service1">
<endpoint behaviorConfiguration="AjaxEndpointBehavior" binding="webHttpBinding" contract="ClientScriptServices.Service1" />
</service>
</services>
</system.serviceModel>
Calling our WCF service from script is now as simple as calling a WebService with the 'ScriptService' attribute:
Listing 14:
var xhr = createXHR();
xhr.open("POST", "Service1.svc/Echo", true);
xhr.onreadystatechange = function()
{
if (xhr.readyState === 4)
{
}
};
xhr.setRequestHeader("content-type", "application/json");
var postData = '{"input": "Hello World!"}';
xhr.send(postData);
Listing 15:
var xhr = createXHR();
xhr.open("POST", "Service1.svc/EchoTestObject", true);
xhr.onreadystatechange = function()
{
if (xhr.readyState === 4)
{
}
};
xhr.setRequestHeader("content-type", "application/json");
var postData = '{"input": { "Name": "Foo", "Age": 21 }}';
xhr.send(postData);
Warning: webHttpBinding leverages DataContractSerializer which is a bit more strict in the flavor of JSON it will consume. If you are not using a standards based library such as json2.js to construct your JSON postData you must be ensure the following points or you will run into trouble:
- All keys are double quoted
- All non-value type values are double quoted.
Caveat: By default, the current HttpContext is not available to a WCF service. This means that the current Session and FormsAuthentication are not available. This behaviour can be altered with the addition of the configuration element 'system.serviceModel/serviceHostingEnvironment' and the service attribute 'AspNetCompatibilityRequirements' as show by the fragments in Listings 16 and 17. The implications and usage of this functionality will be covered in a later document.
Listing 16:
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
< ... >
</system.serviceModel>
Listing 17:
using System.ServiceModel;
using System.ServiceModel.Activation;
namespace ClientScriptServices
{
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceContract]
public class Service1
{
....
}
}
Conclusion
As show in this document, leveraging the full power of the ASP.NET platform from client-side JavaScript can be as simple adding a few lines of code to an existing script obviating the requirements of an ASPX client page with a ScriptManager/MS AJAX js library.
What's Next?
Coming soon:
- Techniques for handling Session State and FormsAuthentication from client script
- A standalone, lightweight, robust AJAX client library fully compatible with ASP.NET endpoints with Session State and FormsAuthentication
History
04-18-2010 - removed licensing restriction