I’ve discussed creating a WCF or ASMX Service proxy for operation with jQuery and without using the Microsoft AJAX libraries. This was some time ago and some things have changed since then – most notably the introduction native JSON parsers in IE 8 and FireFox 3.5 as well as some changes in .NET 3.5 SP1 that makes some JSON transfer tasks a little bit easier in some situations. So I think it’s time to revisit this topic and update the code I showed in the past.
How the WCF ServiceProxy works
The idea of the service proxy is to make it as easy as a single function call to call a WCF or ASMX service. There no dependencies here other than jQuery.js. The goal is to initialize the proxy with a URL and then simply call .invoke() methods to call service methods on the service. The resulting client code using the proxy looks like the following.
Include a script reference into the page in the header:
<script src="../scripts/serviceproxy.js" type="text/javascript"></script>
There are two steps to calling services: Initializing the proxy which can be done globally at the top of the script code and then actually making the service method callbacks.
Start by adding the initialization code in a global location on your page or in a separate external .js file:
// *** Create a static service instance (serviceProxy.js)
// *** which includes ServiceProxy, JSON2, and JSON extensions
var serviceUrl = "BasicWcfService.svc/";
var proxy = new ServiceProxy(serviceUrl);
Then you’re ready to make service calls. The following is the ever illustrious HelloWorld request called from a click button handler:
function showHelloWorld()
{
proxy.invoke("HelloWorld",
{ name: $("#" + serverVars.txtHelloNameId).val() },
function(result) {
$("#divHelloWorldResult")
.text(result)
.slideUp("hide", function() { $(this).slideDown("slow") });
},
onPageError, false);
}
The individual .invoke calls then simply specify the method to call on the server, and an object map with parameter names as properties. So the HelloWorld method has a name parameter hence { name: "Rick" } is sent. If you had multiple parameters the map would have multiple properties: { parm1: "rick", parm2: 10, parm3: new Date() }. Parameter types can be of any type as long as they match in structure what the server’s method expects.
The third and fourth parameters are the success callback which receives the result value, and the failure callback which receives an exception like object. The success handler typically is used to assign the result value to the page somehow by displaying some notification or message, or updating some page content. Here an anonymous function is used inline to handle the success callback which receives the result of the service callback – in this case a string of an HTML fragment – as a parameter. The html is then promptly embedded into the page and made visible with a small effect.
Setting up a WCF Service on the Server
On the server side a WCF Service that implements the service methods using the WCF AJAX style WebScriptServiceHostFactory can be set up like this:
Go into web.config file and REMOVE all the System.ServiceModel entries created from the WCF service
Open the .SVC file created and add the Factory attribute to the page as shown below
<%@ServiceHost Language="C#"
Service="WcfAjax.BasicWcfService"
CodeBehind="BasicWcfService.cs"
Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory"%>
Notice the Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory" definition in the markup page – this is a shortcut for Microsoft AJAX style messaging that allows you to bypass all web.config configuration. In fact you can remove all WCF related entries from the config file if you like once you use the script factory. You can still use the web.config settings – they will override the defaults, but typically that is not required. If you want ASP.NET compatibility you might want to add one thing to the .config file:
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
</system.serviceModel>
All that’s left to do then is to set up the service and service methods. For simplicity I’m just using a class that implements the contract. If you want to be anal and prefer setting up a separate ServiceContract interface for an internal service class that will never be reused be my guest – but for most application internal AJAX services that hardly a necessary practice.
Here’s is the service class:
[ServiceContract(Namespace = "DevConnections")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
#if DEBUG
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
#endif
public class BasicWcfService
{
[OperationContract]
public string HelloWorld(string name)
{
return "Hello " + name +". Time is: " + DateTime.Now.ToString();
}
[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);
}
[OperationContract]
[WebGet]
public Stream GetStockHistoryGraph(string stockSymbols,string title, int width, int height)
{
string[] symbols = stockSymbols.Split(',');
StockServer server = new StockServer();
byte[] img = server.GetStockHistoryGraph(symbols, title, width, height, 2);
MemoryStream ms = new MemoryStream(img);
return ms;
}
[OperationContract]
public DateTime AddDays(DateTime date, int days)
{
return date.AddDays(days);
}
[OperationContract]
public string ThrowServerException()
{
throw new InvalidOperationException("User generated Error. This error has been purposefully created on the server");
return "Gotcha!";
}
}
Nothing fancy here – just methods marked up with [OperationContract] attributes and a service class that implements the [ServiceContract] and you’re ready to go.
Calling the Service – Another example
For a slightly more involved example let’s call the GetStockQuotes method on the server which returns a result that looks like this:
The result here is returned as an array of objects that contains stock quotes, which is then rendered into the page on the client via a ‘filling the holes’ template approach (there are other ways to accomplish this using templates). Here’s what this request looks like:
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);
}
The code takes an input string of comma delimited symbols, splits them into an array symbols. The symbols array is attached to the parameter map in the call to .invoke() as the input parameter to GetStockQuotes of the service method. The callback handler receives the array of quotes and creates a new list of stock item templates. The ‘template’ is really just an invisible HTML fragment:
<!-- 'Template' used to render each of the stock items -->
<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>
and the code reads this fragment, clones it and then fills in the ‘holes’ with the retrieved data. Each individual StockItem is then added to the the list container (divStockContent) to display the list.
The data for all of this is using JSON and the stock array that is returned from the server looks like this:
{"d":[{"__type":"StockQuote:#StockPortfolio",
"Company":"Microsoft Corpora",
"LastPrice":25.20,
"LastQuoteTime":"\/Date(1253055600000-0700)\/",
"LastQuoteTimeString":"Sep 15, 4:00PM",
"NetChange":0.20,
"OpenPrice":24.95,
"Symbol":"MSFT"},
{"__type":"StockQuote:#StockPortfolio",
"Company":"Intel Corporation",
"LastPrice":19.55,
"LastQuoteTime":"\/Date(1253055600000-0700)\/",
"LastQuoteTimeString":"Sep 15, 4:00PM",
"NetChange":0.19,
"OpenPrice":19.51,
"Symbol":"INTC"}
]
}
Notice that this is Microsoft’s funky encoding format that includes a ‘wrapping’ type d that is consistent WCFs serialization formatting. The serviceProxy client however cleans this up. The data contains various types which are preserved on the client and also back up to the server if you post data back. And notice the funky date format which is also Microsoft specific ("\/Date(999123312)\/"). The serviceProxy properly deserializes these dates as well so result[0].LastQuoteTime will be a date. It also handles proper encoding of dates that are sent to the server so they appear as dates there.
The sample is part of the jQuery samples linked at the bottom of this post if you want to check it out a little closer and play with it for yourself.
Ok so much for the background…
ServiceProxy Implementation
I have previously written about the ServiceProxy implementation, but there have been a number of changes that had to be made to deal with recent changes in the browser world namely the addition of native JSON parsers. The old version I created relied on a customized version of Douglas CrockFord’s JSON2.js file. It was customized to handle the date encoding and decoding. JSON2.js is still required since not all browsers support native JSON parsing (only IE 8 and FF 3.5 currently do), but it’s fully compatible to the native JSON parser’s object model so it’s a perfect fit.
However with the advent of native JSON parsers the ‘customization’ of JSON2.js is no longer possible since the native implementations are fixed and immutable. They are also significantly faster so we definitely want to take advantage of this new feature. JSON parsers allow for a Replacer (stringify) and Reviver (parse) function that can be used to provide some post parsing formatting which allows hooking up the date processing both to the native parsers as well as the JSON2 parser with the same code.
I’ve included a ServiceProxy.js class with this post which provides all 3 items in a single file:
- ServiceProxy class
- JSON2
- JSON2 Extensions
JSON2 is included in this package to have everything wrapped up in a single file for easier portability. Alternately you can also grab ww.jquery.js which also includes all three of these components plus a host of jQuery plug-ins for typical AJAX use.
ServiceProxy Class
Let’s quickly look at the first and last components starting with the ServiceProxy. The service proxy is meant to make the service call easy. It relies on jQuery’s .ajax() function to make the actual callback. The proxy provides a number of useful features:
- Single Method Service Calls
- JSON encoding/decoding
- Consistent Error Handling
The latter is an often overlooked part of using jQuery’s .ajax() function because it provides a poor job of returning error information in a meaningful way. The proxy wraps errors into an object with a simple message regardless of whether you’re dealing with a transmission error, a client error or a service application fault.
Here’s the ServiceProxy class implementation:
this.ServiceProxy = function(serviceUrl) {
/// <summary>
/// Generic Service Proxy class that can be used to
/// call JSON Services generically 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.isWcf = true;
this.invoke = function(method, params, callback, errorHandler, bare) {
/// <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>
/// <param name="isBare" type="boolean">Set to true if response is not a WCF/ASMX style 'wrapped' object</parm>
var json = _I.isWcf ? JSON.stringifyWcf(params) : JSON.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 = JSON.parseWithDate(res);
// Bare message IS result
if (bare)
{ callback(result); return; }
// Wrapped message contains top level object node
// strip it off
for (var property in result) {
callback(result[property]);
break;
}
},
error: function(xhr, status) {
var err = null;
if (xhr.readyState == 4) {
var res = xhr.responseText;
if (res && res.charAt(0) == '{')
var err = JSON.parseWithDate(res);
if (!err) {
if (xhr.status && xhr.status != 200)
err = new CallbackException(xhr.status + " " + xhr.statusText);
else
err = new CallbackException("Callback Error: " + status);
err.detail = res;
}
}
if (!err)
err = new CallbackException("Callback Error: " + status);
if (errorHandler)
errorHandler(err, _I, xhr);
}
});
}
}
The invoke method does all the work along with the success and error handlers of the Ajax functions that fix up the result. The .invoke() method starts by encoding the input object into JSON. Notice that for WCF there’s a special flag used since WCF does not (yet?) understand ISO style date strings as dates. So a special call the JSON.stringifyWcf is made to handle this formatting scenario. If you’re calling an ASMX service set isWcf=false; as ASMX services can deal with ISO dates just fine.
Next the service Url is assigned. The service url points to the .SVC or .ASMX file plus the trailing slash. The method name is appended to this url which routes the method call property on the server to run the appropriate method. Next the ajax call is made. Notice that hte content type is set and all native JSON conversion is turned OFF. That’s right we need to handle the JSON conversion ourselves in order to convert dates back properly into date values via JSON.parseWithDates(). The .ajax callback handler also fixes up the response by stripping out the wrapper root object (d:) from the service response by default. If you are calling a raw REST service that doesn’t wrap results you can pass the isBare parameter as true on the invoke method. If there’s a callback handler mapped it is now called with the fixed up result.
The most complex part of this component is the error handling portion that has to check for the various ways that error information is returned by the $.ajax function. The first check is made for an object in the response text which means a server error was detected and should be displayed .THat’s the easy part – it’s much harder to figure out client errors, timeouts or ‘unknown’ errors and the rest of the code in the error handler deals with this. For you as the user though you always get an object back which in my opinion is how it should be.
JSON Extensions to handle Date Encoding/Decoding
The final piece are the JSON extensions to handle date encoding/decoding on requests. This is done by extending the JSON object created by native browsers or JSON2 and adding methods that use a Replacer or Reviver object in calls to stringify or parse. Here’s what this looks like:
if (this.JSON && !this.JSON.parseWithDate) {
var reISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/;
var reMsAjax = /^\/Date\((d|-|.*)\)[\/|\\]$/;
// original var reMsAjax = /^\/Date\((d|-|.*)\)\/$/;
JSON.parseWithDate = function(json) {
/// <summary>
/// parses a JSON string and turns ISO or MSAJAX date strings
/// into native JS date objects
/// </summary>
/// <param name="json" type="var">json with dates to parse</param>
/// </param>
/// <returns type="value, array or object" />
try {
var res = JSON.parse(json,
function(key, value) {
if (typeof value === 'string') {
var a = reISO.exec(value);
if (a)
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]));
a = reMsAjax.exec(value);
if (a) {
var b = a[1].split(/[-+,.]/);
return new Date(b[0] ? +b[0] : 0 - +b[1]);
}
}
return value;
});
return res;
} catch (e) {
// orignal error thrown has no error message so rethrow with message
throw new Error("JSON content could not be parsed");
return null;
}
};
JSON.stringifyWcf = function(json) {
/// <summary>
/// Wcf specific stringify that encodes dates in the
/// a WCF compatible format ("/Date(9991231231)/")
/// Note: this format works ONLY with WCF.
/// ASMX can use ISO dates as of .NET 3.5 SP1
/// </summary>
/// <param name="key" type="var">property name</param>
/// <param name="value" type="var">value of the property</param>
return JSON.stringify(json, function(key, value) {
if (typeof value == "string") {
var a = reISO.exec(value);
if (a) {
var val = '/Date(' + new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])).getTime() + ')/';
this[key] = val;
return val;
}
}
return value;
})
};
}
The parse() function looks for a regEx match on string values that matches either an ISO or MS Ajax style string. If found that string is parsed into a date value and converted into a date using this Reviver function.
The stringifyWcf() function looks for strings that match the ISO signature. Unfortunatey JSON first turns the date into an ISO date THEN makes the Replacer function. This means the ISO string needs to be parsed into a string first then back into a date. Yuk. Further JSON quote()s the string AFTER it has been converted so it’s actually IMPOSSIBLE to create "\/Date(90232312)\/" style strings in the output. Natch.
By pure luck though I discovered that WCF also supports dates in the format of: "/Date(232133993)/" and that is exactly what the code above does. Lucked out on that one and I wouldn’t be surprised if this is actually an undocumented hack the WCF team added for just this scenario. If it wasn’t for this deviant behavior JSON extension wouldn’t be possible for the full MS Ajax style date (AFAIK) short of creating a custom JSON encoder and forfeiting the native JSON parsers completely.
Ultimately the best solution to this issue will be for WCF supporting ISO string date formats the same way the JavaScript serializer does.
And that’s it.
All of this looks pretty complicated but it’s really not – the whole thing is wrapped in a single ServiceProxy.js file so you can just include that in your project and off you go making service calls. You’ll also need jQuery loaded of course before this ServiceProxy.js since it depends on jQuery for the Ajax callbacks. If you’re using the West Wind West Wind Web Toolkit or ww.jquery.js this functionality is already baked into it as well so no need for a separate add of the ServiceProxy.js.
Why does this matter?
Now, if you’re using Microsoft ASP.NET AJAX with Web Forms and a ScriptManager you’re probably thinking: WTF? This is a big hassle. And yes, this is more work than using a ScriptReference. In fact if you already use ASP.NET AJAX in your application go ahead and use that by all means – it’s easier to use the premade proxy and doesn’t add any additional overhead.
However, more and more there are situations where ScriptManager is not available. MVC applications for example – there’s no ScriptManager and no script references and the closest thing in Microsoft Ajax is Sys.Net.WebServiceProxy.invoke which is very similar in fact to this service proxy (but then one look at this API makes me ill :-}). Or you may simply eschew use of ASP.NET AJAX in general and prefer using only jQuery (or some other library). No need to add ASP.NET AJAX into the mix if it’s just for Web Service calls.
Also keep in mind that there are alternatives to calling WCF/ASMX services. In MVC you might just use plain POST values to send data to the server and use a JsonResult() response. You can also rig something similar with any WebForms page that accepts POST. Or you can use the AjaxMethodCallback control in the West Wind Web Toolkit (links below) which allow page, control or HttpHandler callbacks a little more transparently.
However, personally I still prefer using a completely separate service or component (whether it’s WCF, West Wind Web Toolkit or an MVC Controller specific to Ajax Callbacks) rather than mixing UI and data calls into Page processing logic. But – you can do it either way.
Choices are good and this gives the you the freedom to decide how to handle calls. Even if you’re not using the ServiceProxy for WCF callbacks, I think you might find the $.ajax() wrapper code useful as this really applies to any kind of XHR call to the server – error handling and JSON parsing apply regardless of which technology you use on the server.
Resources
- ServiceProxy.zip
Package that contains ScriptProxy.js and ScriptProxy.min.js which contains ScriptProxy,JSON2, and the JSON extensions described above.
- ww.jquery.js
West Wind jQuery extensions also include all of the above functionality plus a bunch of other commonly used plugins and utility functions. It’s bigger (about 20k compressed) but includes a ton of functionality (docs).
- Samples from jQuery Presentation
The sample discussed is included in these samples as BasicWcfService.aspx. Includes the serviceProxy.js and ww.jquery.js files.
Other Posts you might also like