Using jQuery with ASP.NET
Part 2: Making Ajax Callbacks to the Server
by Rick Strahl
www.west-wind.com/weblog
Last Update
Updated: 12/3/2008
Also available:
In the
first part of this article series I introduced jQuery’s
functionality and how it provides a rich client side programming model.
This time around I’ll expand on these concepts and show you how you can
use jQuery in combination with ASP.NET using it as an AJAX backend to
retrieve data. I’ll also discuss how you can create ASP.NET controls and
otherwise interact with jQuery content from ASP.NET pages in WebForms.
jQuery is just a JavaScript library so it will work
seamlessly with ASP.NET both from page code as well as through backend driven
code using the Page.ClientScript object or ScriptManager. You can use jQuery on
its own as a client side and Ajax library that communicates with ASP.NET or you
can use jQuery in combination with ASP.NET AJAX. The two actually complement
each other quite well as jQuery provides functionality that the ASP.NET AJAX
library does not and vice versa. For the most part the interaction between the
two libraries is trouble free except for a few very rare edge cases.
In this article I’m not going to be talking much about
ASP.NET AJAX since that’s been covered ad finitum in other places – the
procedure doesn’t vary much if you’re using it with jQuery. Instead I’ll focus
on using only jQuery plus a few small helpers to
make callbacks to the server easily. In the process you’ll get to see how some
of jQuery’s AJAX features work and how to manage the data coming back from the
server in a few different ways.
First Ajax Steps with jQuery
One of the most obvious client side features of any
Javascript client library is the ability to make AJAX calls to the server.
jQuery includes a host of Ajax functions that make it easy to retrieve content
from a Url starting with the low level and do-everything $.ajax() function
plus a number of others that are simpler
and more focused to specific tasks. Here’s a list of some of the functions
available:
$.ajax(opt)
This the low level Ajax function
with many, many options that lets you create just about any kind of Ajax
request. If you need full control over requests or you want to create a generic
component that calls back to the server (like the WCF/ASMX client proxy I’ll
discuss later) you’ll want to use this function. For now
check out the
documentation on the multitude of options available.
$(sel).load(url,data,callback)
The .load() function is the only Ajax function that works
off a jQuery selector. It calls a URL on the server and loads the result as
content into selected element(s). It’s a very quick and easy way to load Html
fragments and inject them into the document if your result is HTML. An optional
callback can be provided to be notified with the server result text when the
callback completes which is useful if you want to visually adjust the retrieved
content – like applying an effect to visually cue the user to an update. Note
this function is heavily overloaded: If no URL is specified .load() acts as a
load event handler that fires when an element has loaded its data (ie. an image
or script).
$.get(url,callback),$.post(url,data,callback)
These functions are simple helpers that provide basic get and post
operations to the server. You specify a URL and a callback which is called with
the HTTP response from the server. $.post() also allows you to pass either
formatted POST buffer string or an object the properties of which are turned
into POST encoded key value pairs.
$.getJSON(url,data,callback)
Similar to $.post(), but expects the result to be JSON
which is automatically deserialized into a Javascript value and passed to the
callback as a parameter. While this function is handy for simple JSON results
there are two things to watch out for: Dates are not parsed since there’s no
date literal in Javascript, so whatever the server returns will be used
(typically a string). $.getJSON() also doesn’t support JSON POST data – only
POST encoded variables. This function is useful for simple JSON results returned
from arbitrary services, but not usable for calling WCF or ASMX ASP.NET services
since they expect JSON POST input. More on this later in the article.
.getJSON() also supports cross domain JSONP callbacks. If
you specify a query string parameter of callback=? you can force the result to
be routed to the callback you specify in the parameter list.
$.getScript(url,callback)
This function loads script code from the server and
executes it once downloaded if no callback is specified. If specified the
optional handler is fired instead and passed the Javascript, plus the current
ajax request. This can be useful for JSONP cross domain callbacks where you have
no control over the parameters used.
Global Ajax Events
There also a number of global Ajax events that you can take
advantage of all of which take callbacks as parameters: ajaxCallback(),
ajaxError(), ajaxSend(), ajaxStart(),ajaxStop(),ajaxSuccess(). These are useful
for setting up global handlers that can centrally manage Ajax requests. You’re
not likely to need these much unless you build components that need to know
status of requests.
Getting started with $().load()
The load() function is the easiest way to retrieve content
from the server and display it. Let’s look at a simple stock quote example from
the included code samples. It retrieves a stock quote from Yahoo and displays
the results using the .load() as shown in Figure 1.
Figure 1: A
simple example loading a stock quote as HTML with the .load() function.
This page works by utilizing two HTML pages: One to hold
the input form (StockAjaxLoad.aspx) and one to display the actual stock chart
(StockDisplay.aspx). The StockDisplay form is a standalone server page that
renders an HTML fragment and is one that is called from jQuery to return the
fragment with the completed stock data.
The stock display
form can be accessed simply via query string parameters by providing a symbol
name, or providing an action=Chart key value pair to renders the stock chart
image. The two Urls the form accepts look like this:
StockDisplay.aspx?Symbol=msft
StockDisplay.aspx?Symbol=msft&action=Chart
The form itself is a plain server form with a few label
controls. The codebehind loads up a stock quote by using a StockServer class
that goes out to Yahoo from the server and retrieves stock data, parsing it into
a stock quote object. The resulting quote data is then assigned to the label
fields. The page is not a full HTML document but rather just a fragment that
starts with a <div> tag and <table> -
there’s no <html> or <body> tag since the output will be merged into the
existing document. Check out and test the StockDisplay.aspx to see how the
display and stock retrieval works.
The key part of the client page contains only the form
fields and a placeholder is very simple and looks like this:
<div
class="containercontent">
Stock Symbol:
<asp:TextBox
runat="server"
ID="txtSymbol"
Text="MSFT"
/>
<input
type="button"
id="btnSubmit"
value="Get Quote"
onclick="showStockQuote();"
/>
<img
src="../images/loading_small.gif"
id="imgLoading"
style="display:
none"/>
<hr
/>
<div
id="divResult"
style="width:420px"></div>
</div>
To load a stock quote with jQuery we can now very easily
read the symbol and use the .load() function to load the result into divResult.
<script
type="text/javascript">
function showStockQuote() {
$("#divResult").load("StockDisplay.aspx",
{ symbol: $("#txtSymbol" ).val() });
}
</script>
That’s it! We call .load() with the URL plus the optional
data parameter which can be either a string (a raw POST buffer) or an object
map, the properties of which turn into POST parameters. Here I’m passing an
object with a single symbol property that is turned into a POST var.
On the server form the StockDisplay.aspx fragment page code
simply picks up the symbol, retrieves the stock quote and updates the HTML
display and renders the page. Listing 1 shows the StockDisplay codebehind that
handles both the stock display and stock chart image.
public
partial class
StockDisplay : System.Web.UI.Page
{
protected void
Page_Load(object sender,
EventArgs e)
{
string action = Request.Params["Action"];
if (action ==
"Chart")
this.GetStockChart();
this.GetStockQuote();
}
void GetStockQuote()
{
string symbol = Request.Params["Symbol"]
?? "";
StockServer stockServer =
new StockServer();
StockQuote quote =
stockServer.GetStockQuote(symbol);
this.lblCompany.Text = quote.Company +
" - " + quote.Symbol;
this.lblPrice.Text =
quote.LastPrice.ToString();
this.lblOpenPrice.Text =
quote.OpenPrice.ToString();
this.lblNetChange.Text =
quote.NetChange.ToString();
this.lblQuoteTime.Text =
quote.LastQuoteTimeString;
// *** this will call this page to retrieve the
chart
this.imgStockQuoteGraph.ImageUrl =
"StockDisplay.aspx?Symbol=" +
quote.Symbol +
"&action=Chart";
}
void GetStockChart()
{
string symbol = Request.Params["Symbol"]
?? "";
StockServer stockServer =
new StockServer();
byte[] img =
stockServer.GetStockHistoryGraph(
new
string[1] { symbol },
"Stock History for " + symbol,
400, 250, 2);
Response.ContentType = "application/jpg";
Response.BinaryWrite(img);
Response.End();
}
}
Note that the single page handles both the text and image
displays by routing via query string and post variables and a little routing.
That was easy. Using .load() to call an external page is
childishly simple.
But we probably should improve the sample a little bit. The
first thing to do is to make the display a little more interactive. When you
load a stock quote when one is already active it’d be nice to use an effect to
‘clear’ the old quote and display the new one. We can use the .load() method and
its callback parameter to coordinate sliding the display up and then down again
once the quote has loaded completely. This takes a little coordination as
Listing 2 demonstrates.
function showStockQuote() {
var div = $("#divResult");
showProgress();
div.slideUp(function() {
div.load("StockDisplay.aspx",
{ symbol: $("#txtSymbol").val() },
function() {
$(this).slideDown();
showProgress(true);
});
});
}
function showProgress(hide)
{
var img = $("#imgLoading");
if (hide)
img.hide();
else
img.show();
}
ASP.NET ClientIds and jQuery Selectors
In the code above I’m simply using the ID of the txtSymbol
control and that works fine in this particular example, because the txtSymbol
control is not contained inside of any ASP.NET naming container like a
MasterPage or UserControl. If it did the code above would fail because the ID
would not be found.
Specifically inside of a master page you might find that
the ID gets mangled by ASP.NET into:
ctl00_Content_txtSymbol. I could change my code to read:
{ symbol: $("#ctl00_Content_txtSymbol").val() }
which works, but is pretty ugly and volatile because the
parent IDs might change if a container is renamed or moved around.
Another option is to use a little server side script markup
to embed the ClientID:
{ symbol: $("#<%=
txtSymbol.ClientID %>").val() }
This is also ugly, but reliable. But this does not work if you end up moving
your code into a separate .js script file. If you use client ids like this a lot
you might create a list of them as global variables:
var txtSymbolId = "<%=
txtSymbol.ClientID %>";
which then lets you reuse the variable a little more
easily:
{ symbol: $("#" + txtSymbolId).val()
}
These variables are also visible in loaded script files.
Finally, some time ago I created a
ScriptVariables component that allows for
creating variables on the server and rendering them into the client. Among its
features this component can expose all control client IDs as object properties
under a named object. You can find out more about this component in
this blog post.
Using this approach you’d end up with a server variable of your choosing that
can be referenced like this:
{ symbol: $("#" +
serverVars.txtSymbolId).val() }
All of this is pretty ugly and might make you want to consider using master
pages and user controls in script heavy pages, but the workarounds are fairly
easy, just tedious.
In the future ASP.NET 4.0 will bring some relief in this
regard with a new Control property that lets you specify how control IDs are
rendered with an option to override INamingContainer name mangling.
Using .load() with the same ASPX page
In the last stock example I used a separate ASPX page to
display a result, which is great if what you’re rendering warrants a self
contained page that you might reuse. But often you simply want to render some
HTML to load into the page by simply calling back to the current page to keep
things self contained. Having a separate page (or even a user control) for each
AJAX callback certainly can be overkill.
However, you can use pretty much the same process used in
the last example to call back to the same page. The main difference is that
you’ll need to do a little bit of routing to determine whether you are in a
callback or whether you’re rendering the full page as a standard postback.
Let’s look at another example that demonstrates this Page
callback process. In this example, I’ll use some server side control rendering
to render a ListView Control with data and return the Html fragment result back
to the client. Again the client page uses $(sel)..load(). Figure 2 shows what
the sample looks like.
Figure 2: A
server side control’s HTML loaded into the client page via .load()
The layout of this portion of the sample is very simple. It
contains merely the dropdown list plus a placeholder that receives the result
from the server.
<fieldset>
<legend>.load() with ASP.NET Control</legend>
<div
class="fieldsetpadding">
Show
Entries:
<asp:DropDownList
runat="server"
id="lstFilter"
onchange="showEntries()">
<asp:ListItem
Text="Recent Items"
Value="Recent" />
<asp:ListItem
Text="All Items"
Value="All" />
<asp:ListItem
Text="Open Items"
Value="Open" />
</asp:DropDownList>
<a
href="javascript:{}"
id="lnkShowEntries">Go</a>
<div
id="divEntryDisplay"
style="margin:
10px;display:none;">
</div>
</div>
</fieldset>
The page also contains a non-visible ListView control and
some containing markup that is not initially rendered:
<!-- EntryList 'Template' rendered for AJAX
retrieval -->
<asp:PlaceHolder
runat="server"
ID="entriesPlaceHolder"
Visible="false">
<div
class="blackborder"
style="width:500px;">
<div
class="gridheader">Select
one of the Open Entries</div>
<div
style="height:
300px;
overflow-Y:
scroll; overflow-x:
hidden;">
<asp:ListView
runat="server"
ItemPlaceholderID="layoutContainer"
ID="lstEntries">
<LayoutTemplate>
<div
id="layoutContainer"
runat="server"
/>
</LayoutTemplate>
<ItemTemplate>
<div
id="itemtemplate"
onclick="alert('Clicked
Entry: ' + $(this).attr('pk')); return false;"
pk="<%#
Eval("pk") %>">
<div
id="clockimg"></div>
<b><a
href="javascript:
alert('clicked');"><%#
Eval("Title")
%></a></b><br
/>
<small><%#
Eval("Timein")
%></small>
</div>
</ItemTemplate>
</asp:ListView>
</div>
<div
id="divListStatus"
class="toolbarcontainer"
>
<asp:Label
runat="server"
ID="lblListCount"></asp:Label>
</div>
</div>
</asp:PlaceHolder>
When the selection changes in the list, the showEntries
Javascript function runs and calls the server to retrieve the list based on the
value selected in the filter drop down:
function showEntries()
{
var filter = $("#"
+ scriptVars.lstFilterId).val();
var jDiv = $("#divEntryDisplay");
jDiv.load("simplepageCallbacks.aspx?Callback=EntryList",
{ Filter: filter },
function(result,status,xhr) {
jDiv.slideDown(1000);
$("#lnkShowEntries").text("hide");
});
}
On the server side the key to making page callbacks into
the same page is routing. Notice the URL I’m using above which includes a
Callback=EntryList query string parameter. This sample page contains several
different callbacks and each of them has a unique Callback id that I’ll use on
the server side to route to the appropriate page method to process the callback
appropriately. Listing 3 shows the server page that includes the routing logic
public
partial class
Ajax_SimplePageCallbacks : System.Web.UI.Page
{
protected void
Page_Load(object sender,
EventArgs e)
{
// *** Route to the Page level callback
'handler'
this.HandleCallbacks();
}
// Callback routing
void HandleCallbacks()
{
string callback = Request.Params["Callback"];
if (string.IsNullOrEmpty(callback))
return;
// *** We have an action try and match it to a
handler
if
(callback == "HelloWorld")
this.HelloWorldCallback();
else if
(callback == "EntryList")
this.EntryListCallback();
else if
(callback == "StockQuote")
this.GetStockQuote();
else if
(callback == "StockHistoryChart")
this.GetStockHistoryChart();
Response.StatusCode = 500;
Response.Write("Invalid Callback Method");
Response.End();
}
void EntryListCallback()
{
string Filter = Request.Params["Filter"]
?? "";
// *** Render the data into the listview
this.LoadEntryList(Filter);
// *** Render just the list view into html
string html =
WebUtils.RenderControl(this.entriesPlaceHolder);
Response.Write(html);
Response.End();
}
void LoadEntryList(string
Filter)
{
TimeEntryContext context =
new TimeEntryContext();
IQueryable<TimeEntry>
q =
from ent in
context.TimeEntries
orderby ent.TimeIn
descending
select ent;
if (Filter ==
"Recent")
q = q.Take(10);
else if
(Filter == "Open")
q = q.Where(ent => !ent.PunchedOut);
else if
(Filter == "Closed")
q = q.Where(ent => ent.PunchedOut);
// *** Need a concrete instance so we can count
List<TimeEntry>
custList = q.ToList();
this.lblListCount.Text =
custList.Count.ToString() + " Entries";
this.entriesPlaceHolder.Visible =
true;
this.lstEntries.DataSource = custList;
this.lstEntries.DataBind();
}
}
The routing is very simple – the Page_Load() early on calls
HandleCallbacks() which looks at the Callback query string variable. If passed
it goes into callback processing otherwise the code simply resumes normal page
processing. If it is a callback the callback takes over page processing which
results in full page output being sent and – eventually – a Response.End() to
fire to complete page processing resulting in only the partially rendered list.
HandleCallbacks simply maps the Callback Id to a specific
method in the Page class. If I add a new callback all I have to do is add
another Id and map it to another method. Each method should return a full HTTP
response – including potentially managing errors.
The EntryList processing loads data from Linq to SQL into
the ListView control by running a query and data binding the ListView with data.
Once bound a utility routine (provided with the samples) called
WebUtils.RenderControl() is used to render the entire PlaceHolder containing the
list view and headers and return just the HTML output. There’s also a
WebUtils.RenderUserControl() which allows you to specify a user control to load
dynamically and render. RenderControl is actually quite simple:
public
static string RenderControl(Control
control)
{
StringWriter tw =
new StringWriter();
// *** Simple rendering - just write the control to
the text writer
// *** works well for single controls without
containers
Html32TextWriter writer =
new Html32TextWriter(tw);
control.RenderControl(writer);
writer.Close();
return tw.ToString();
}
It works great for list controls like ListViews and
Repeaters or simple containers like PlaceHolder. But there are a few caveats: It
will only work with simple controls that don’t post back to the server even when
you don’t plan on using any Postback operations. For more complex controls or
containers you’ll have to use the more complete RenderUserControl method in the
same WebUtils library. You can read more
about these two
routines in this blog post and review the code in the samples download.
Rendering controls is only one way to generate the HTML
fragment output of course. You can hand of course code HTML output, or generate
output from canned routines in your library – it doesn’t matter where the HTML
comes from as long as you can return it back as
string. The same SimplePageCallbacks.aspx page contains a couple of other
examples that return generated HTML data in a few of other ways.
Returning JSON from a Page Method
To demonstrate returning data, let’s reuse the stock
display example, but instead of returning HTML let’s return a JSON string back.
JSON stands for Javascript Object Notation and it’s an object representation
format that Javascript recognizes and can evaluate natively without any manual
parsing code. JSON string are simply evaluated and if valid results in a
Javascript value or object.
If you look back at HandleCallbacks one of the routes calls
the GetStockQuote() function in the page which should look familiar. Here though
I use the ASP.NET AJAX JavaScriptSerializer() class to create JSON on the server
and return it to the client:
void GetStockQuote()
{
string symbol = Request.Params["Symbol"]
?? "";
StockServer stockServer =
new StockServer();
StockQuote quote = stockServer.GetStockQuote(symbol);
JavaScriptSerializer ser =
new
JavaScriptSerializer();
string res = ser.Serialize(quote);
Response.ContentType = "application/json";
Response.Write(res);
Response.End();
}
This code is very simple. It generates the stock quote as
before by using the StockServer business object that retrieves the quote data
from Yahoo and parses it into a StockQuote object. I then use the
JavaScriptSerializer to turn this StockQuote object into a JSON string.
The generated JSON string of the serialized StockQuote
looks like this:
{"Symbol":"VDE","Company":"VANGUARD
ENRGY ET",
"OpenPrice":0,"LastPrice":67.97,
"NetChange":0.00,
"LastQuoteTime":"\/Date(1227578400000)\/",
"LastQuoteTimeString":"Nov
24, 4:00PM"}
This JSON is sent to
the client which requests it by calling back to the ASPX page with the Callback=GetStockQuote
querystring. The code in Listing 4 demonstrates making a jQuery .getJSON()
callback to retrieve and the display the stock quote.
function getStockQuote()
{
var symbol = $("#"
+ scriptVars.txtSymbolId ).val();
$.getJSON("SimplePageCallbacks.aspx?Callback=StockQuote",
{Symbol: symbol },
function(result) {
$("#StockName").text(result.Company);
$("#LastPrice").text(result.LastPrice.formatNumber("c")
);
$("#OpenPrice").text(result.OpenPrice.formatNumber("c")
);
$("#NetChange").text(result.NetChange.formatNumber("n2")
);
$("#QuoteTime").text(result.LastQuoteTimeString
);
$("#divStockDisplay").slideDown("slow");
});
$("#imgStockQuoteGraph")
.attr("src",
"SimplePageCallbacks.aspx?Callback=StockHistoryChart"
+
"&symbol=" + encodeURIComponent(symbol))
.fadeOut( function() { $(this).fadeIn(1000)
});
}
Notice how .getJSON() receieves the result parameter which
is the deserialized StockQuote object. The anonymous function that handles the
callback simply assigns the object property values to the appropriate DOM
elements. Note that a few helper functions are used
from the ww.jquery.js support library you
can find in your samples. Here the formatNumber function is used to format
numbers to the proper numeric display.
Those pesky JSON Dates
.getJSON() works great in simple scenarios like this where
you only receive a JSON result. I’m lucky however in that the date I’m
interested in is provided as a string (on purpose <g>). I’m not using the
LastQuoteTime property of the stock quote, but rather the preformatted string
version LastQuoteTimeString which is generated on the server.
Take a look at the LastQuoteTime date in the JSON string
above again and notice how it’s formatted. The issue is that Javascript does not
a have a standard Date literal, so there’s no effective way to embed a date into
JSON that is universally recognizable as a date by a parser. Microsoft uses a
string that’s marked up like this:
"LastQuoteTime":"\/Date(1227578400000)\/"
This value is a special string encoding format that starts
with slashes and then has a pseudo date function that contains milliseconds
since 1/1/1970, which is the Javascript 0 date. The format is easy to recognize
and parse which is why I suspect Microsoft created it, but it’s still a string,
so if you read this value with .getJSON() you’d get back… a string rather than a
date. Unless you have a parser on the client that understands this date format
the date isn’t parsed and .getJSON() won’t parse it for you.
I’ll talk about how to address the date issue in the next
section, but for now just keep in mind that JSON is a nice lean way to return
data back and also send data back to the server.
JSON and Service Based Ajax
Returning JSON is a great way to build client centric
applications. One of the big draws of returning and also passing back JSON to
the server is that you can make very small and atomic accesses and updates to
the server with very little data travelling over the wire. HTML is bulky, where
JSON is much more precise and results in much smaller payloads in most cases.
Using JSON amounts to a more client centric approach to
User Interface development. Rather than using the server to generate HTML from
the current page or other pages, you can use the server as a service to return
you only data. You can then use client side script code to update or create your
user interface which given the flexibility that jQuery provides can often be
much easier than generating the same server side HTML.
It’s also easier to create JSON on the server side and the
approach I showed is only one of ways that you can generate JSON. Since we’re
talking about a data format often times you don’t need to use ASP.NET pages (or
MVC views for that matter) to generate JSON – instead you can use light weight
modules or as we’ll see next WCF or ASMX web services.
And this is a perfect segue into the next section.
Using jQuery with WCF and ASMX Services
If you are planning on using .NET as a data service to
return data to client applications, there is a rich service infrastructure in
the form of Windows Communication Foundation (WCF) or ASMX Web Services
available. Both platforms as of .NET 3.5 support exposing services to JSON
natively. If you’re using .NET 2.0 the ASP.NET AJAX Server Extensions 1.0 can
also be used to provide the same functionality.
When using either WCF or ASMX JSON services you have the
choice of using ASP.NET AJAX to call these services using the
ASP.NET ScriptManager to provide the client service calling infrastructure.
I’m not going to cover this method in this article since this is covered in
plenty of other places and doesn’t really affect jQuery usage. When you use
ScriptManager and the client proxy generated by it you can simply call the Web
Service based on the proxy generated using the class and methods exposed by it.
You can then use jQuery to apply the retrieved data as shown here or in Part 1
of this article series. I have also provided the
BasicWcfServicesScriptManager.aspx example that mirrors the jQuery only code
I’ll describe in the next section.
Creating a WCF Service for AJAX Consumption
To create a WCF REST service that can be called with AJAX
callbacks you need to do the following:
1. Add a new WCF
Service to the Web Application as shown in Figure 3.
Figure 3 – Adding a new WCF
service to your project
which results in a new .svc file and codebehind file to be added to your
project. Make sure that your Web Application or Project is a .NET 3.5 based
project since only .NET 3.5 supports WCF REST Services.
Once you’ve added the service you should see the service in the Web Application
Project as shown in Figure 4. Note that if you are using stock Web Projects
(rather than WAP as shown) the CodeBehind and interface file will be located in
your APP_CODE folder instead.
Figure 4 –
the .SVC service as shown in a Web Application.
2. Open up the .SVC file in markup mode
add a the WebScriptServiceHostFactory as follows:
<%@
ServiceHost Language="C#"
Service="WcfAjax.BasicWcfService"
CodeBehind="BasicWcfService.cs"
Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory"
%>
This host factory is pre-configured to set ASP.NET AJAX
style messaging that allows ASP.NET AJAX as well as your jQuery clients to
effectively and consistently communicate with the server. The
WebScriptServiceHostFactory configures requests in such a way that all requests
must be made with POST and expect JSON objects with parameter properties as
input, and wrapped JSON objects as output. Also, any service errors are returned
as exception objects rather than raw HTML messages allowing you to effectively
marshal service exceptions to the client.
3. Remove any Service configuration
settings related to the new service from web.config. When the service was
added by default it was added with wsHttpBinding which is a SOAP binding that
won’t work with AJAX. Remove all related entries as WebScriptServiceHostFactory
provides all the necessary configuration settings for you. You can still
configure the service later with custom binding behavior settings if necessary,
but in most cases the default behavior of the factory is sufficient.
If you’d like to use ASP.NET Compatibility in your Web
Service to be able to access the HttpContext.Current object in your code the
same way as ASMX services did you can add the following setting into the
web.config file:
<system.serviceModel>
<serviceHostingEnvironment
aspNetCompatibilityEnabled="true"/>
</system.serviceModel>
4. Set up your Service Class. By
default the WCF wizard will set up your service contract interface and
implementation class as two separate classes. Traditionally in WCF you define
your service contract in the interface where you specify the [OperationContract]
and other attributes and any custom behaviors. You then create a class that
implements the interface that provides the actual operational implementation.
While you’re free to implement the contract interface and class separately, for
AJAX services I prefer to implement only a service class that implements both
the contract and implementation on a single class as shown in Listing
6. Unlike typical services that can and
often are reused with multiple protocols and different hosts and clients, AJAX
services tend to be single focus application services and so to my pragmatic
view at least don’t benefit from the extra layer of abstraction – I can’t
foresee reusing my AJAX contract anywhere but in the local app. If you do then
keep them separate. Having a single class to work with in a changeable AJAX
environment is much more productive.
namespace WcfAjax
{
[ServiceContract(Namespace
= "BasicWcfService")]
[AspNetCompatibilityRequirements(RequirementsMode
=
AspNetCompatibilityRequirementsMode.Required)]
#if DEBUG
[ServiceBehavior(IncludeExceptionDetailInFaults
= true)]
#endif
public class
BasicWcfService
{
[OperationContract]
public
StockQuote GetStockQuote(string symbol)
{
StockServer server =
new StockServer();
StockQuote
quote;
return server.GetStockQuote(symbol);
}
[OperationContract]
public
StockQuote[] GetStockQuotes(string[]
symbols)
{
StockServer server =
new StockServer();
return server.GetStockQuotes(symbols);
}
}
}
Using only jQuery to call WCF
To call this service only using jQuery we’ll need to do a
little bit of work. While jQuery has support for basic JSON functionality in the
form of the .getJSON() function,
this method isn’t adequate for calling the WCF or ASMX services properly.
There are two problems with .getJSON(): It doesn’t support client side
JSON serialization natively and it doesn’t know how to deal with ASP.NET style
date formats. To address this we’ll need:
- MS AJAX aware JavaScript JSON Serializer
jQuery does not include a JSON
serializer. Since WCF/ASMX require parameters to be sent as POST JSON
objects, a serializer is required on the client. The standard getJSON()
deserialization also doesn’t work with the MS date format ("LastQuoteTime":"\/Date(1227578400000)\/")
so special handling is required for deserialization as well. For this
task I’ve provided a modified version of Douglas Crockford’s
JSON2.js or
ww.jQuery.js both of which include JSON parsers that understand the
Microsoft date format both for serialization and deserialization.
- A custom Service Callback Handler
Making a WCF callback requires setting quite a number of options on the
$.ajax() function and the results coming back have to be handled properly in
order to yield consistent values. WCF results need to be ‘unwrapped’ and any
$.ajax() errors need to be normalized so that an error object is returned.
To facilitate this process I’ve provided the small ServiceProxy class in
Listing 5 as well as in
ww.jquery.js (which provides a slightly more complete version).
Let’s take a look and see what this looks like when calling
the above Web Service using the helpers I mention above. In the following
example I allow the user to enter a set of stock symbols and retrieve a set of
StockQuotes objects that are then rendered into a list view like display on the
client as shown in Figure 5. This list is client rendered.
Figure 5 –
Example of a WCF Service providing data to a jQuery client with client
rendering.
Unlike the Page examples earlier, in this example the
server only provides JSON data rather than HTML to the client. The client
renders the result by using an empty ‘fake template’ that exists in the HTML
document and filling in the ‘data holes’ with the data retrieved from the server
for each of the retrieved stock quotes.
Listing 7 shows the Javascript code used to make the
callback to the server and handle the updating of the display when the callback
returns using the ServiceProxy class.
// *** Create a global instance
var serviceUrl =
"BasicWcfService.svc/";
var
proxy = new ServiceProxy(serviceUrl);
function getStockQuotes() {
var symbols = $("#"
+ serverVars.txtSymbolsId ).val().split(",");
proxy.invoke("GetStockQuotes",
{ symbols: symbols },
// pass symbol array as 'symbols' parameter
function(quotes) {
// result is an array for each of the symbols
var jCnt = $("#divStockDisplay").fadeIn("slow");
var jDiv = $("#divStockContent").empty();
// quotes is an array
$.each(quotes, function(index) {
var jCtl = $("#StockItemTemplate").clone();
jCtl.attr("id",
"stock_" + this.Symbol);
var symbol =
this.Symbol;
jCtl.find(".itemstockname").text(this.Company);
jCtl.find("#tdLastPrice").text(this.LastPrice.formatNumber("n2"));
jCtl.find("#tdOpenPrice").text(this.OpenPrice.formatNumber("n2"));
jCtl.find("#tdNetChange").text(this.NetChange.formatNumber("n2"));
jCtl.find("#tdTradeDate").text(this.LastQuoteTimeString);
jCtl.fadeIn().click(function() { alert('clicked
on: ' + symbol); });
jCtl.find(".hoverbutton").click(function(e)
{
alert("delete clicked on: " +
symbol); e.stopPropagation(); });
jDiv.append(jCtl);
});
},
onPageError);
}
function onPageError(error){
alert("An error occurred:\r\n" +
error.Message);
}
The sample page uses the ServiceProxy class for the callback a global instance
of this class is created. ServiceProxy receives the URL of the service to call
including the trailing backslash when instantiated. Once instantiated the
.invoke() method of the proxy instance can be called to make a callback to the
server.
When calling the GetStockQuotes method the code first
retrieves the input field value containing the comma delimited symbols and
splits them up into an array as the server method expects. I use a shortcut here
for Client Ids by way of a custom control called ScriptVariables (provided in
jQueryControls.dll) that exposes all ClientIds as properties of the serverVars
object. So serverVar.txtSymbolsId contains the ClientID of the txtSymbols
control. The user enters symbols as a comma delimited list which is .split() and
turned into an array of symbol strings that the service method excepts as an
input parameter.
Next the service is called with proxy.invoke(). You pass
this method the name of the service method to call plus any parameters which are
provided in the form of an object with each parameter a property of the object.
So the service method is defined like this:
[OperationContract]
public
StockQuote[] GetStockQuotes(string[]
symbols)
{
StockServer server =
new StockServer();
return server.GetStockQuotes(symbols);
}
On the client I’m passing an object that has a symbols
property with the symbols array as a value:
{ symbols: symbols }
If you had multiple parameters you’d express them as an
object with multiple properties with each property matching the server parameter
names:
{ symbol:
"MSFT", years: 2 }
In the GetStockQuotes call the invoke method serializes the
symbols array into JSON and sends it to the server for processing. The server
side method in the WCF service class receives the symbol array and then
retrieves a set of quotes as an array that is returned to the client. The JSON
result is an array of StockQuote objects which WCF returns like this:
{"d":
[{"__type":"StockQuote:#WcfAjax",
"Company":"LDK
SOLAR CO ADR",
"LastPrice":15.48,
"LastQuoteTime":"\/Date(1227913260000-1000)\/",
"LastQuoteTimeString":"Nov
28, 1:01PM",
"NetChange":1.35,
"OpenPrice":14.40,
"Symbol":"LDK"
},
{"__type":"StockQuote:#WcfAjax",
"Company":"MKT
BCTR GBL ALT",
"LastPrice":21.00,
"LastQuoteTime":"\/Date(1227913140000-1000)\/",
"LastQuoteTimeString":"Nov
28, 12:59PM",
"NetChange":0.44,
"OpenPrice":20.27,
"Symbol":"GEX"
}
]
}
Note the “root level wrapped property” that’s typical of
WCF and ASMX services and accounts for the “wrapped” format I’ve been talking
about. The root property then contains the actual result, which is an array of
two stock quote objects. When the actual result is returned to the client the
callback should receive only the actual result value which is the array of
quotes. The ServiceProxy class takes care of this task, so when the callback is
made successfully, only the array of
quotes is passed to the callback function not the whole evaluated WCF structure.
If you look back on Listing 7 you see that the third
parameter is a callback handler and it receives the result as an array of quote
objects. The code that follows then parses through the array and effectively
adds new items into the following placeholder in the HTML document:
<div
id="divStockDisplay"
class="blackborder"
style="display:none;width:
600px;">
<div
class="gridheader">Stock
Results</div>
<div
id="divStockContent"
style="overflow-y:
scroll;height:
200px;">
</div>
</div>
The code starts out by making the stock display visible and
fading it in as it’s initially not displayed. Then all the content in
divStockContent is cleared out since we will be loading a new set of items into
the display container. Next the code loops through each of the quote objects
using jQuery’s static $.each() function. $.each() loops over each item and calls
the specified function in the context of the item parsed – in this case a
StockQuote which is exposed as the this
pointer.
Inside of the .each loop then code then proceeds to load up
the ‘template’. I call it a template, but really it’s just a fragment of hidden
HTML in the page that holds the empty layout of a stock quote without data:
<div
id="StockItemTemplate"
class="itemtemplate"
style="display:none">
<div
class="stockicon"></div>
<div
class="itemtools">
<a
href='javascript:{}'
class="hoverbutton"
><img
src="../images/remove.gif"
/></a>
</div>
<div
class="itemstockname"></div>
<div
class="itemdetail">
<table
cellpadding="3"><tr>
<td>Price:</td>
<td
id="tdLastPrice"
class="stockvaluecolumn"></td>
<td>Open:</td>
<td
id="tdOpenPrice"
class="stockvaluecolumn"></td>
<td>Change:</td>
<td
id="tdNetChange"
class="stockvaluecolumn"></td>
<td
id="tdTradeDate"
colspan="2"></td>
</tr></table>
</div>
</div>
The code picks up the empty template from the page and simply clones it, which
creates a new DOM element:
var jCtl = $("#StockItemTemplate").clone().show();
The result is a new jQuery object of that new as of yet
unattached DOM element. One important thing to set is the ID property of the
item so that each element can be uniquely identified later, so
var ctl = jCtl.attr("id","stock_"
+ this.Symbol;
accomplishes that task. The code then stuffs the stock
quote data into the appropriate ‘holes’ in the template by using the find
command and setting the .text() of the element like this:
jCtl.find(".itemstockname").text(this.Company);
Finally when the item has been all updated it gets added to
the bottom of the list with:
jDiv.append(jCtl);
which adds the newly cloned and formatted element to the
content container. Voila, we’ve just called a WCF service with an array input
and output value and nice jQuery logic to dynamicly render the results on the
client.
The ServiceProxy simplifies Service Calls
The code above is not much more involved than the code
you’d write with a ScriptManager generated proxy. The main difference is that
there is no proxy (and no Intellisense) and you end up calling the .invoke()
method with a method string and object for parameters rather than a simple
method on the proxy. The rest of the behavior is pretty much the same.
The ServiceProxy class is basically a wrapper around the
jQuery $.ajax() function that uses a slightly modified version of JSON2.js to
handle serialization and deserialization and ‘unwrapping’ of success and error
responses so the behavior is consistent with object/value results returned in
all situations. Listing 5 shows the relatively short implementation of the
ServiceProxy class.
this.ServiceProxy =
function(serviceUrl) {
/// <summary>
/// Generic Service Proxy class that can be used to
/// call JSON Services using jQuery.
/// Depends on JSON2.js modified for MS Ajax usage
/// </summary>
/// <param name="serviceUrl" type="string">
/// The Url of the service ready to accept the method
name
/// should contain trailing slash (or other URL
separator ?,&)
/// </param>
/// <example>
/// var proxy = new ServiceProxy("JsonStockService.svc/");
/// proxy.invoke("GetStockQuote",{symbol:"msft"},
///
function(quote) { alert(result.LastPrice); },onPageError);
///</example>
var _I = this;
this.serviceUrl = serviceUrl;
this.invoke = function(method,
params, callback, error) {
/// <summary>
/// Calls a WCF/ASMX service and returns the
result.
/// </summary>
/// <param name="method" type="string">The
method of the service to call</param>
/// <param name="params" type="object">An
object that represents the parameters to pass {symbol:"msft",years:2}
/// <param name="callback"
type="function">Function called on success.
/// Receives a single parameter of the parsed
result value</parm>
/// <param name="errorCallback"
type="function">Function called on failure.
/// Receives a single error object with Message
property</parm>
// Convert input data into JSON - REQUIRES
modified JSON2.js
var json = JSON2.stringify(params);
// Service endpoint URL
var url = _I.serviceUrl + method;
$.ajax({
url: url,
data: json,
type: "POST",
processData: false,
contentType: "application/json",
timeout: 10000,
dataType: "text",
// not "json" we'll parse
success: function(res) {
if (!callback)
return;
// Use json library so we can fix up MS AJAX
dates
var result = JSON2.parse(res);
// Wrapped message contains top level object
node
// strip it off
for (var
property in result) {
callback(result[property]);
break;
}
},
error: function(xhr) {
if (!error)
return;
var res = xhr.responseText;
if (res && res.charAt(0) ==
'{')
var err = JSON2.parse(res);
if (err)
error(err);
else
if (xhr.status != 200)
error({ Message: "Http Error: " +
xhr.statusText });
else
error({ Message: "Unknown Error Response"
});
return;
}
});
}
}
Most of the work is handled by the jQuery $.ajax() function
that performs the actual AJAX callback. The .invoke method first manually
serializes the input parameters using JSON2 before making the $.ajax() call. $.ajax()
calls either a success or error function when implemented. On success the HTTP
string result is unpacked, deserialized and the first ‘property’ value is used
as the result value that is passed to the user provided callback function.
The biggest chunk of code deals with errors. $.ajax()
errors can come in several different forms from protocol level errors to errors
in the service code and the error code basically checks for a JSON object which
WCF returns on service level errors. If a protocol error occurs the response is
HTML and so the status code is retrieved and returned.
Handling Server Side Exceptions
The way error handling works means that you can throw
exceptions on the server and receive those exceptions on the client as part of
the error callback. Consider the following service method:
[OperationContract]
public
string ThrowServerException()
{
throw new
InvalidOperationException(@"User generated Error.
This error has been purposefully created on the server");
return "Gotcha!";
}
which can be called and produce expected results with this
code on the client:
function
throwServerException() {
proxy.invoke("ThrowServerException",
null,
function(result) {
alert("This should never be called.");
},
function(error){
alert("An error occurred:\r\n\r\n" +
error.Message);
});
}
In this case the error function is called error.Message is
going to be “User generated Error. This error…”.
Using WCF in combination with the ServiceProxy makes it
very easy to create new callbacks on the server: Create a method in the service
and then simply call with the ServiceProxy class’s .invoke() method.
If you want to take a look at a more involved sample
application you can check out the JsonStockClient.aspx example, which uses the
ServiceProxy class create a rich user interface entirely on the client side
against a WCF Service interface.
Figure 6 –
The StockPortfolio Sample application demonstrates a host of WCF features with
the ServiceProxy jQuery client.
This application lets you retrieve stock quotes and manage
a simple stock portfolio that lists current pricing of stocks that you have
added into the portfolio. A number of the concepts I’ve discussed are used in
this project as well as the WCF functionality that allows returning image
streams from the WCF service which is used for graphs.
Creating Client Content with Templates
One other feature of interest in this example is templating.
I’ve mentioned templating repeatedly in the first part of the article and again
earlier when I used the ‘data holes’ templating approach of cloning content and
then embedding it into the page. While this approach works it’s still fairly
work intensive as you have to explicitly use jQuery expressions to find the
‘holes’ in the template and fill in the blanks.
There are a number of jQuery templating solutions
available. I’ve used jTemplates (which has a Python like templating language)
successfully for some time, but I’ve recently switched over to a customized
version of John
Resig’s Micro Templating Engine which is very compact but uses plain
JavaScript to handle the templating. The beauty of John’s engine is that it’s
tiny and entirely uses a language that you should already be familiar with –
Javascript.
Because it’s so compact I’ve integrated the Micro
Templating Engine with a few small modifications into my client library (ww.jquery.js).
My customizations from John’s code change the template delimiters from <% %> to
<# #> to avoid issues with ASP.NET’s page parser, add some basic error handling
and display and fix a small bug that has to do with quote characters. Listing 6
shows my version of the parseTemplate() function that is based on
John’s original
base code.
/// based on John Resig's Micro Templating engine
var _tmplCache = {}
this.parseTemplate =
function(str, data) {
var err = "";
try {
var func = _tmplCache[str];
if (!func) {
var strFunc =
"var p=[],print=function(){p.push.apply(p,arguments);};"
+
"with(obj){p.push('" +
str.replace(/[\r\t\n]/g, " ")
.replace(/'(?=[^#]*#>)/g, "\t")
.split("'").join("\\'")
.split("\t").join("'")
.replace(/<#=(.+?)#>/g, "',$1,'")
.split("<#").join("');")
.split("#>").join("p.push('")
+ "');}return p.join('');";
func = new Function("obj",
strFunc);
_tmplCache[str] = func;
}
return func(data);
}
catch (e) { err = e.message; }
return "< # ERROR: "
+ err.htmlEncode() + " # >";
}
The idea of a template engine is that you can create markup
as you normally do, but embed the data as Javascript expressions right into the
content using tag delimiters. Hey we know how to do that already in ASP.NET with
<% %> tags. The difference is we want to do this on the client rather than on
the server.
For example, in the StockPortfolio application the
individual user portfolio items are rendered using the parseTemplate() function.
When the page first loads no data is rendered into the list. Instead client
script requests the portfolio items as an array which is then parsed one item at
a time using the template. The same template is also used to update items when
the user changes a quantity or symbol or adds new items.
Templates allow you to design the layout and data display
in one place and re-use that template, potentially in multiple places.
The template for an individual portfolio item looks like
this:
<script
type="text/html"
id="StockItemTemplate">
<div class="itemtemplate" style="display:none">
<div
class="stockicon"></div>
<div
class="itemtools">
<a href="javascript:{}" class="hoverbutton">
<img src="../images/remove.gif" /></a>
</div>
<div
class="itemstockname"><#= Symbol #> - <#= Company #></div>
<div
class="itemdetail">
<table style="padding: 5px;"><tr>
<td>Last Trade:</td>
<td id="tdLastPrice" class="stockvaluecolumn"><#= LastPrice.formatNumber("n2")
#></td>
<td>Qty:</td>
<td id="tdLastQty" class="stockvaluecolumn"><#= Qty #></td>
<td>Holdings:</td>
<td id="tdItemValue" class="stockvaluecolumn"><#= ItemValue.formatNumber("c")
#></td>
<td id="tdTradeDate" colspan="2"><#= LastDate.formatDate("MMM dd, hh:mmt")#></td>
</tr></table>
</div>
</div>
</script>
Note the <script> tag which is a great way to hide the
template from normal HTML markup. The above is valid HTML and any XHTML parser
and robot will simply ignore this script block as if it wasn’t there. Yet the
content of the script – the template string - can still be retrieved based on
its id. This template is passed a stock quote object and the properties of the
quote are accessed inside the <#= #>
expressions. The expressions are plain Javascript expressions and you can see
that some of the values call functions like formatNumber and formatDate which
are part of the ww.jquery.js library. You can also embed code blocks into the
page to loop through items which I’ll show a little later.
Let’s look and see how the client code can retrieve the
stock quote data. Listing 7 shows the code to retrieve the portfolio items and
then iterate through them and merge them into the template one at a time.
function LoadQuotes(noMsg)
{
proxy.invoke("GetPortfolioItems",
{ userToken: userToken },
function(message) {
$("#lstPortfolioContainer").empty();
$.each(message.Items, function(i) {
var item = this;
// this is the iterated item!
var
newEl = UpdatePortfolioItem(item);
});
},
OnPageError);
}
function
UpdatePortfolioItem(stock) {
// Retrieve the Item template
var
template = $("#StockItemTemplate").html();
// Parse the template and merge stock data into it
var html
= parseTemplate(template, stock);
// Create jQuery object from gen'd html
var newItem = $(html);
// See if we have an existing item
var origItem = $("#"
+ stock.Pk + "_STOCK");
if (origItem.length < 1)
newItem.appendTo("#lstPortfolioContainer");
else
origItem.after(newItem).remove()
newItem.attr("id", stock.Pk +
"_STOCK")
.click(function() { ShowStockEditWindow(this)
})
.fadeIn(1500);
return newItem;
}
There’s not a lot of code in this sample and that’s really
the point. The only thing of interest in regards to templating is the retrieval
of the template and then merging it with these two lines of code:
var
template = $("#StockItemTemplate").html();
var html
= parseTemplate(template, stock);
The first line simply returns the template as a string from
the script block. The second then merges the stock quote into the template and
returns a string result of the merged data for an individual stock item. The
html is then turned into a jQuery object that then either replaces an existing
item or adds a new one to the list. The UpdatePortfolioItem() function is called
from LoadQuotes() as shown, but it’s also called from UpdatePortfolioItem()
which updates or adds new items to the list. So this function and the template
are re-used in multiple locations without duplicate code or markup.
One piece of code, one piece of markup all maintained in
one place – that’s the benefit of using templates.
parseTemplate() can also work with code blocks so you can
do things like loop through a list. The following is another example provided in
BookAdmin.aspx. One of the forms displays a search result from Amazon as list
that looks as shown in Figure 7.
Figure 7 –
The Amazon Book list uses client side Javascript templating to render the list
In the previous example I used a single item template to
add one item at a time to the list and used Javascript code to loop through the
items. In this example, a single html string is generated for the entire list
based on the template and the template does the iteration. The following
template uses code blocks and a for loop to run through all of the books.
<script
type="text/html"
id="amazon_item_template">
<# for (var i=0; i<bookList.length; i++) {
var
book = bookList[i];
#>
<div class="amazonitem" ondblclick="selectBook(this);"
tag="<#= book.Id #>">
<img
src="<#= book.SmallImageUrl #>" class="imgAmazonSearch"/>
<div><b><#= book.Title #></b></div>
<div><i><#= book.Publisher #>
(<#= book.PublicationDate #>)</i></div>
<small><#= book.Author #></small>
</div>
<# } #>
</script>
Note the <# #> blocks that allow any sort of Javascript
code to be embedded. parseTemplate effectively works by taking the template and
turning it into an executing Javascript function and so just about all
Javascript code can be used in code blocks. You can loop, you can use if
statements to display content conditionally and you can access any global code
that is in scope.
Templating is an extremely powerful mechanism for building
rich client applications without having to generate HTML by hand, filling holes
explicitly and most importantly not repeating yourself when creating HTML
markup.
parseTemplate() is only one templating mechanism but the
general concepts are similar in most template engines. There are a host of other
template solutions available for jQuery. I’ve also used
jTemplates for quite some time
prior to this implementation and it works well, and there are several other
engines available that I haven’t used.
Using AjaxMethodCallback in jQueryControls
The BookAdmin example actually uses yet another callback
mechanism which is provided as part of the jQueryControls project. This project
contains an AjaxMethodCallback control which can make JSON callbacks to the same
page, a user or server control on the same page or an external HttpHandler to
handle JSON callbacks. The control is based on a combination of client script
and a server wrapper control that combine to provide the callback capability.
The server control is optional – you can also use the AjaxMethodCallback client
library on its own and it operates similar to the way ServiceProxy works.
The easiest way to use this functionality is to use the
control and drop it onto the page or insert it via markup:
<ww:AjaxMethodCallback ID="Proxy"
runat="server"
/>
This uses all default settings which allow to call back to
the current page and post back only method parameters. Alternately you can call
an external handler and specify how POST data is to be sent.
<ww:AjaxMethodCallback ID="Proxy"
runat="server"
ServerUrl="AdminCallbackHandler.ashx"
PostBackMode="PostNoViewstate"
/>
In the BookAdmin page callbacks are made using an ASHX
handler which is the most efficient way, but realize that the methods of the
handler shown below could easily have been created as methods of the current
form.
Like WCF and ASMX services you can create simple methods
that are called from the client and receive JSON parameters and return a JSON
response. Here’s is a single method in the BookAdmin.ashx handler
implementation:
///
<summary>
/// Callback
handler for Amazon Books Page
///
</summary>
public
class AdminCallbackHandler :
CallbackHandler
{
private busBook
books = new
busBook();
[CallbackMethod]
public List<AmazonBook>
GetBooks(string filter)
{
if (filter ==
null)
filter = string.Empty;
IQueryable<AmazonBook>
bookList = null;
if (filter ==
"Highlighted")
bookList = books.GetHighlightedBooks();
else if
(filter == "Recent")
bookList = books.GetRecentBooks();
else
bookList = books.GetBooks();
return bookList.ToList();
}
}
The handler needs to inherit from CallbackHandler which
provides all the necessary plumbing to marshal the request to the appropriate
method, call it and return a result back to the client.
On the client the server code is easy to call and uses a
client proxy that works in the same way that the WCF/ASMX proxy works. The proxy
is created with the same name as the AjaxMethodCallback control’s Id –
Proxy in this case and has methods for
each of the exposed server methods. Each method has the same parameter signature
as the server method plus the callback and errorCallbacks – just like service
proxies discussed earlier. To call the server is as easy as:
function editBook(ctl)
{
var bookPk = $(ctl).attr("id").replace("book_","");
bookPk = parseInt(bookPk);
Proxy.GetBook(bookPk,
function(book) {
// object result
alert(book.Title + " " + book.AmazonUrl);
ShowBookForm(book);
},
onPageError);
}
The actual page code in the sample of course is a bit more
complex and it uses jQuery to pop up and display the edit window and populate
its form fields. What is nice about AjaxMethodCallback is that it’s fully self
contained and loads up jQuery.js and the ww.jquery.js script library (optionally
– you can override resource load behavior) so you drop the control add methods
and you’re on your way, and it doesn’t require any of the ASP.NET AJAX or WCF
components.
One additional benefit of this control is also the ability
to use it to handle callbacks in page and control code. This means if you build
a custom ASP.NET server control that needs to retrieve data from the client
directly (rather than through a service reference), it’s easy to do by simply
adding an AjaxMethodCallback to the page’s controls collection and pointing it
at the class that will handle callbacks. Any class can be pointed at to handle
callback events.
Summary
Phew – a lot of options for you to calling back to the
server using jQuery. In this article I’ve covered both manually using ASP.NET
pages to handle callback routing and returning both HTML and JSON data to the
client. I’ve also covered three – count ‘em – mechanisms for making JSON based
server callbacks that let you pass data between client and server.
Which approach is best? As always it depends.
For existing applications that need to add a little bit of
AJAX functionality page level callbacks that return HTML fragments can be a
quick way to provide little bits of content to update the client. Control or
user control rendering can provide reuse of server side components, or you can
generate HTML from scratch to return to the client. Personally I prefer the
‘raw’ Ajax approach which uses the server as a data service for serving JSON
data to the client and then update the client using either small chunks of DOM
manipulation or for more complex or markup intensive items using templates. It’s
a little more work but you can do so much more interesting stuff and you have
way more control over your user interface.
When it comes to callbacks to the server you have lots of
choices. I discussed three callback mechanisms – WCF, ASMX and
AjaxMethodCallback. WCF or ASMX are no brainers if you’re already using ASP.NET
AJAX. If you’re not using ASP.NET AJAX you can still use either WCF/ASMX on the
server and use the ServiceProxy class and the JSON parsers I introduced in the
article on the client. Doing so lets you avoid loading the ASP.NET AJAX client
library which is overkill if you only need to make service callbacks.
AjaxMethodCallback is very easy and fully
self contained so you can just drop a control and you’re ready to go. It also is
a bit more flexible since you can use the server component on a Page, a user or
server control, in a high performance Http Handler or even your own custom
handling code via the generic JsonCallbackMethodProcessor class which does all
the dirty work of parsing input and output and generating JSON.
All approaches make it easy to make callbacks to the server
with literally just a single line of code, so that you can focus on using the
data returned effectively and use jQuery to update the UI. After all that’s
really what it’s all about – service callbacks should be just plumbing and the
logic of applying the results is where the real work usually comes in and that’s
where jQuery really excels. If you’re building complex or repetitive UI layouts
client side make sure you check out templating for merging Javascript based
templates with data. Templates make sure you don’t repeat yourself and keep your
layout in one place even though you are working on the client side.
I hope Part 2 of this article has been useful to you. In
Part 3 I’ll look a bit closer at integration between ASP.NET and jQuery in terms
of creating server controls. I touched on this topic a little with the
AjaxMethodCallback control which integrates client jQuery components and
plug-ins and a server control. I’ll look deeper into how you can build reusable
ASP.NET components that interact with jQuery. Until have fun calling server side
code with jQuery.
Resources
Sample Download:
Components described in this article are
also part of the
West Wind Web Toolkit for
ASP.NET Talk Back
jQueryControls
Web Sites
Printable Cheat
Sheets
Books
© Rick Strahl, West Wind Technologies, 2008
|