Home / AJAX

Page History: How Do I Retrieve Data from a Page Method?

Compare Page Revisions



« Older Revision - Back to Page History - Newer Revision »


Page Revision: 2010/09/01 02:07


Problem

I have an ASP.NET WebForms page and I want to call an AJAX Page Method without using the ASP.NET AJAX libraries generated proxies using only jQuery.

Solution

Page methods are a special implementation of ASP.NET AJAX service callbacks that can be hosted inside of an ASP.NET WebForms page subclass. Page methods need to be static and like their full blown ASP.NET AJAX ASMX service counterparts take JSON POST input and return JSON result values. Page methods are a simplified way of making callbacks without separately setting up an ASMX or WCF Web Service.

Page Methods are simple RPC methods that take JSON POST data inputs and return a JSON encoded string of the result value from the method call and they can be readily called from jQuery AJAX calls from the client.

The input is a JSON object that contains one property for each parameter passed to the method, and the result returns a root object with a ‘d’ property that contains the actual result value. The reason for the ‘d’ property is security to avoid JSON execution attacks that can be run against JSON arrays.

Setting up a Page Method on the Server

Lets assume we have a simple form on the client with a textbox and a button to retrieve a simple HelloWorld message from the server. The idea is to enter your name, send it to the server and the server responds back with a HelloWorld message string. Here’s the base HTML to work with:

 
<div class="sample">
    <h3>Hello World with Page Methods</h3>
    <div class="containercontent">
        Enter your name: 
        <asp:TextBox runat="server" ID="txtName" Text="" />
        <input type="button" id="btnHelloWorld" value="Say Hello"  />
                
        <div id="divHelloMessage" class="errordisplay" style="display:none"></div>
    </div>
</div>

The whole simple UI including the callback message once retrieved looks like this:

Image

Try Me!

To implement the Page Method on the server place a method into CodeBehind Page subclass that handles the callback when the button is clicked. This highly sophisticated HelloWorld method looks like this:

[WebMethod]
public static string HelloWorld(string name)
{
    return "Hello " + name + ". Server time is: " + DateTime.Now.ToString();
}

Notice the required WebMethod attribute (found in the System.Web.Services assembly and namespace) and that the method has to be static. Other than that it’s just a standard .NET method with input parameters and a result type – in this case both strings – which are parsed to and from JSON by ASP.NET AJAX.

Calling a Web Method from the Client

Calling a Page Method (or any service that requires JSON input including ASMX and WCF services) requires that the parameters for the method call are POSTed as a JSON string to the server. jQuery natively doesn’t support JSON encoding and so an additional library may be required to handle JSON encoding. I say ‘may’ as the latest versions of browsers do include native JSON serializers including Internet Explorer 8, FireFox 3.5 and later, Opera 10.5 and later, Chrome 5, Safari 5 and even the iPhone 4’s Safari browser. However, older browsers do not and so you still require a JavaScript library to support non-current browsers.

JSON Serialization Requirements

The most commonly used JavaScript library for JSON encoding and parsing is json2.js by Douglas Crockford. This version is so popular that the native JSON implementations in browsers is based on this API. You can get the latest version of json2.js from here:

http://www.json.org/json2.js

jQuery does support native JSON parsing via $.getJSON() and $.ajax() with the dataType: "JSON" option set, however as we’ll see shortly this deserialization doesn’t deal natively with date values and so custom parsing may still be required if you care about getting dates de-serialized on the client.

Calling a Page Method with the $.ajax() Function

Once you have JSON serialization and parsing covered the next step is to make the AJAX call to the server. jQuery supports making AJAX calls and can retrieve an AJAX result from the server directly using the low level $.ajax() function.

On the client then we have to do a few things to call this method. Start with the script includes for jQuery and json2.js:

<script src="Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
<script src="Scripts/json2.min.js" type="text/javascript"></script>

It’s also possible to load json2.js conditionally instead of the script include above by dynamically loading the script only if needed:

<script type="text/javascript">
    if (!window.JSON)
        $.ajax({ type: "GET",url: "Scripts/json2.min.js",dataType: "script"});                                           
</script>

Next let’s hook up the btnHelloWorld click handler in the document ready handler:

$(document).ready(function () {
    $("#btnHelloWorld").click(helloWorldCallback);
});

Finally let’s create the actual function that makes the service call using $.ajax() and updates the client display:

function helloWorld() {
    var name = $("#txtName").val();

$.ajax({ url: "PageMethods.aspx/HelloWorld", // Current Page, Method data: JSON.stringify({ name: name }), // parameter map as JSON type: "POST", // data has to be POSTed contentType: "application/json", // posting JSON content dataType: "JSON", // type of data is JSON (must be upper case!) timeout: 10000, // AJAX timeout success: function (result) { $("#divHelloMessage").text(result.d).fadeIn("slow"); }, error: function (xhr, status) { alert(status + " - " + xhr.responseText); } }); }

Try Me!

You can see that there are quite a few options that need to be set on the $.ajax() call. The most important parts are the URL which is the page url plus an extra path that hold the method to call. You also need to pass parameters as a JSON encoded object with each method parameter encoded as a property of the object passed using the JSON objects (native or json2’s stringify() function). The resulting JSON data needs to be posted to the server and the content type has to be set to application/json. To get the result data automatically parsed into a value/object you need to specify the dataType: "JSON" which lets jQuery handle de-serialization of the JSON result and pass it to the callback method.

Finally you need to provide success and error callbacks that are called when the AJAX call completes. The result handler receives the service method’s result as a value/object. Note that the returned value used is result.d to get the result string from the Helloworld call:

$("#divHelloMessage").text(result.d);

This is because ASP.NET AJAX (and WCF as well) create response messages that look like this:

{"d":"Hello Rick. Server time is: 8/30/2010 11:55:24 AM"}

All .NET JSON service responses include this ‘wrapped’ message format to provide some basic security to prevent JSON hijacking attacks (for more info see http://tinyurl.com/6xnp8d). Just remember that you need to reference the “d” property to get the result value.

The result value can be something simple like a string in this case, but can also be more complex like a complex nested object or array depending on what the server Page Method returns.

The error handler is a bit more problematic as there is only minimal error handling support in the error function. The function returns only a terse error code about the general failure that occurred (ie. timeout, error, parsererror etc. which isn’t very descriptive) and the XmlHttpRequest object. The latter can be useful to pick up the server response and any error messages the server might have sent since ASP.NET AJAX encodes exception information into the response as an object. More on this later in the document.

Simplifying Callbacks with jQuery Service Proxy

While the $.ajax() call described above is not rocket science it’s pretty verbose and there are a few shortcomings that could be smoothed out nicely with a small service wrapper. Specifically the following can be handled much cleaner by a wrapper:

  • Automatic JSON encoding
  • Automatic fix up of the “d” result value for a cleaner response
  • Automatic date parsing from the default encoded strings
  • Simplified error handling

Let’s look at another example that demonstrates retrieving a more complex result from the server – a stock quote that contains current market information about a stock symbol with last trade info and also a trade time.

Try Me!

On the server the Page Method looks like this:

[WebMethod]
public static StockQuote GetStockQuote(string symbol)
{
    if ( string.IsNullOrEmpty(symbol) )
    	throw new ArgumentException("A symbol is required");

StockServer stockServer = new StockServer(); return stockServer.GetStockQuote(symbol); }

The StockQuote class that holds the quote data looks like this:

public class StockQuote
{
    public static DateTime DATE_EMPTY = 
            new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    public string Symbol {get; set; }
    public string Company {get; set; }
    public decimal LastPrice {get; set; }
    public decimal NetChange { get; set; }        
    public DateTime LastQuoteTime
    {
        get { return _LastQuoteTime; }
        set { _LastQuoteTime = value; }
    }
    private DateTime _LastQuoteTime = DATE_EMPTY;
    public string LastQuoteTimeString
    {
        get { return LastQuoteTime.ToString("MMM d, h:mmtt"); }            
    }
}

And the StockServer class that handles the stock retrieval via Yahoo’s finance service and parses the CSV result strings looks like this (you can find more documented code in the code downloads for this sample):

public class StockServer
{
    private const string STR_YAHOOFINANCE_STOCK_BASEURL = 
              "http://download.finance.yahoo.com/d/quotes.csv?s=";
    private const string STR_STOCK_FORMATTING = "&f=sl1d1t1c1ohgn";

public StockQuote GetStockQuote(string symbol) { StockQuote quote = null; WebClient http = new WebClient(); string quoteString = http.DownloadString(STR_YAHOOFINANCE_STOCK_BASEURL + symbol + STR_STOCK_FORMATTING); quote = this.ParseStockQuote(quoteString); return quote; }

private StockQuote ParseStockQuote(string QuoteString) { StockQuote Quote = new StockQuote();

string[] Details = QuoteString.Split(','); Quote.Symbol = Details[0].Replace("\"", ""); Quote.Company = Details[8].Replace("\"", "").Replace("\n", "") .Replace("\r", "").Trim();

string Work = Details[1]; decimal WorkNumber = 0M; decimal.TryParse(Work, out WorkNumber); Quote.LastPrice = WorkNumber;

Work = Details[4]; WorkNumber = 0.00M; decimal.TryParse(Work, out WorkNumber); Quote.NetChange = WorkNumber;

Work = Details[2] + " " + Details[3]; Work = Work.Replace("\"", ""); DateTime WorkDate = DateTime.UtcNow; DateTime.TryParse(Work, out WorkDate);

if (WorkDate < StockQuote.DATE_EMPTY) return null; else Quote.LastQuoteTime = WorkDate;

return Quote; } }

The HTML layout for this request looks like this:

<div class="containercontent">
    Enter a stock Symbol:
    <asp:TextBox runat="server" ID="txtSymbol" Text="" />
    <input type="button" id="btnStockQuote"  value="Get Quote"/>

<div id="divStockQuote" class="errordisplay" style="display:none;"> <div class="label">Company:</div><div id="stockname"></div> <div class="label">Last Price:</div><div id="stockvalue">x</div> <div class="label">Net Change:</div><div id="stockchange">x</div> <div class="label">Last Update:</div><div id="stocklastupdate">x</div> </div> </div>

First let’s set this up to run again with the $.ajax() call as we did for the HelloWorld request earlier:

Try Me!

function getStockQuote_ajaxOnly() {
    var symbol = $("#txtSymbol").val();

$.ajax({ url: "PageMethods.aspx/GetStockQuote", // page method data: JSON.stringify({ symbol: symbol }), // parameter map type: "POST", // data has to be POSTed contentType: "application/json", timeout: 10000, dataType: "json", success: function (quote) { quote = quote.d; $("#divStockQuote").show(); $("#stockname").text(quote.Company + " (" + quote.Symbol + ")"); $("#stockvalue").text(quote.LastPrice); $("#stockchange").text(quote.NetChange); $("#stocklastupdate").text(quote.LastQuoteTimeString); }, error: function (status, xhr) { debugger; alert("An error occurred"); } }); }

Let’s examine the response returned from this request when we pass MSFT for the symbol. I’ll use FireFox and the popular FireBug plugin to monitor the XHR traffic and display the JSON view on the request:

Image

You can clearly see the “d” root object returned along with all of the properties defined on the StockQuote object. One issue you see here is that the date format returned is a specially formatted string. As mentioned earlier native JSON parsing doesn’t convert dates for you so result.d.LastQuoteTime will actually return the string value you see in the callback code above. For this reason the StockQuote object also contains a LastQuoteTimeString property which formats the quote date on the server and passes it to the client. This is one approach to deal with client date formatting – let the server do it and push the string value to the client as part of the JSON message result or view model that returned to the client.

The other issue relates to error handling. If we call the Callback with no symbol specified we can cause the server to generate an error by throwing an exception. The exception is serialized and passed back to the client:

Image
In this case the XmlHttpRequest.responseText contains an error and the error is described in the Message property of the result object. In other error scenarios however, like a timeout or an authorization failure no server response is returned. So error processing can be a bit tricky.

By using a ServiceProxy wrapper (## Link to example code ##) to make service calls date and error handling, the “d” object fix-up and the entire service call can be made much simpler. Using the ServiceProxy class the GetStockQuote call above can be reduced to:

Try Me!

// global instance of a Service Proxy to make remote calls
proxy = new ServiceProxy("PageMethods.aspx/");

function getStockQuote_serviceProxy() { var symbol = $("#txtSymbol").val(); showError();

proxy.invoke("GetStockQuote", { symbol: symbol }, function (quote) { $("#divStockQuote").show(); $("#stockname").text(quote.Company + " (" + quote.Symbol + ")"); $("#stockvalue").text(quote.LastPrice); $("#stockchange").text(quote.NetChange); $("#stocklastupdate").text(quote.LastQuoteTime); }, function (error) { alert(error.Message); });

}

To use the Proxy class you create an instance of the ServiceProxy class and pass in the base Url for the page Page/ASMX/WCF and then use the .invoke() method to call the service by passing simply the method to call, the parameters as an object map and implementing the callback and error handlers as needed. The callback method receives the result object without the “d” sub object and properly parses Date values into real JavaScript dates. The error handler always receives an error object which contains a Message property so you don’t have to check Status messages or probe the Xhr response text on every hit.

The ServiceProxy is made up of three distinct pieces:

  • The ServiceProxy class which is a wrapper around the $.ajax() call
  • JSON object extensions that handle ASP.NET AJAX and ISO date fixups
  • jQuery AJAX dataFilter that allows post parsing of data

You can examine the ServiceProxy class and the converters and helpers in ServiceProxy.js in the scripts folder of the sample solution (##insert CodePaste URL once we have a live link to samples##).

Using a service wrapper simplifies your service calls as you don’t have to remember all of the $.ajax() options that have to be set correctly and it provides functionality that natively is missing in raw callbacks. It also provides a small layer of abstraction which makes it easier to inject additional functionality in the future as well as possibly switch out the callback mechanism entirely to something else if you choose.

The ServiceProxy class also works with ASMX and WCF services (if you use WCF set the isWcf property to true to allow for special date encoding) in addition to Page Methods.

Summary

Page Methods are an easy way to create callbacks to the server by creating methods in your CodeBehind Page subclass. These methods are convenient for small callbacks that retrieve small bits of data that are page specific. Although they are easy to use and set up and functionally very similar to full service implementations, I would recommend that you use Page Methods sparingly especially if you end up implementing many service calls per Page. Implementing callbacks as ASMX service handlers is more efficient and provides a level of separation between your HTML page’s UI and your callback logic. Service based methods also tend to be more lightweight requiring less resources and performing slightly faster.

All the content covered in this topic also applies almost identically to ASMX services and WCF services. The server side setup for WCF services is a bit different but the client side calling code works the same so all concepts shown here can be applied against services in the same way.

About the Author

Rick Strahl is the big Kahuna at West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development, develops several commercial and free tools, provides training and mentoring with focus on .NET, IIS and Visual Studio. Rick's a C# Microsoft MVP, a frequent contributor to magazines and books, and a frequent speaker at developer conferences and user groups. He is also co-publisher of CoDe magazine. For more information please visit his blog at www.west-wind.com/weblog/ or contact Rick at rstrahl@west-wind.com.