My dataType:json
call to WSEzPL8.asmx/GetCompaniesJSON for a jQuery auto-complete dropdownlist list inside a jQuery dialog works great (just in case anyone is having problems getting their AjaxControlToolkit ComboBox
inside an AjaxControlToolkit ModalPopupExtender
to work). Fiddler shows JSON requests and responses in all their compact glory and Google → Tools → JavaScript console shows:
XHR finished loading: "http://localhost:49220/WSEzPL8.asmx/GetCompaniesJSON".
That's nice that my own server plays nice with itself, but I want to do some cross-site scripting from my development server's client web page to my production server's web service using JSONP with jQuery. I know browser security won't let it happen unless I use something like Rick Strahl's Adsense Exploit.
My rudimentary first attempt was to just try adding a P on the end of JSON dataType with no XSS. What's peculiar is Fiddler shows identical (I think) JSON requests and responses for both json and jsonp. Except, the page tain't work'n no mo. So close, yet so far...
Google → Tools → JavaScript console shows an:
Uncaught Syntax Error: Unexpected Token.
"http://localhost:49220/WSEzPL8.asmx/GetCompaniesJSON?callback=jQuery15208622294222004712_1318613867031".
I never toked on anything so I was caught somewhat flat footed in how to respond when the computer starts token on its own --just because I put a little p in it.
To answer my own question, you can't just swap POST ⇆ GET and json ⇆ jsonp.
JSONP supposedly only works with GETs. ASMXs normally only work with POSTS. groups.google.com/group/jquery-dev/ says, "Cross-domain JSONP isn't AJAX at all. It doesn't use XMLHttpRequest
. It's nothing more than a dynamic script element that loads JavaScript code."
$(function () {
$("#MainContent_Company").autocomplete({
source: function (request, response) {
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "../WSEzPL8.asmx/GetCompaniesJSON",
dataType: "json",
data: $.toJSON({ term: request.term, maxRows: 12 }),
contentType: "application/json; charset=utf-8",
async: true,
cache: true,
success: function (data) {
response($.map(data.d, function (item) {
return {
label: item.label,
value: item.value
}
}))
}
})
},
minLength: 2,
select: function (event, ui) {
$("#MainContent_CoID").val(ui.item ? ui.item.value : "");
$("#MainContent_Company").val(ui.item.label ?
ui.item.label : "");
return false;
},
open: function () {
$(this).removeClass("ui-corner-all").addClass("ui-corner-top");
},
close: function () {
$(this).removeClass("ui-corner-top").addClass("ui-corner-all");
},
focus: function (event, ui) {
$("#MainContent_Company").val(ui.item.label);
return false;
},
change: function (event, ui) {
$("#MainContent_CoID").val(ui.item ? ui.item.value : "");
}
});
});
});
Ah Ha! JSONP surrounds the regular JSON response with the name of the callback function that called it, hence the newly introduced token. Duh --that's exactly what Matthew Dennis kinda explained above.
What's cool is, the jQuery autocomplete javascript doesn't change significantly between JSON and JSONP. You really can swap POST ⇆ GET and json ⇆ jsonp with one additional change on the client side. The.ASHX handler code below doesn't return the d root that ASP.Net web services do, so remove it from mapping the response data into the local javascript object.
response($.map(data.d, function (item) {...
There is one significant change required to handle JSONP: instead of calling the web service in an .ASMX methods page we call an .ASHX handler page.
While .ASHX uses JavaScriptSerializer to de-serialize the request and serialize from LINQ to JSON for the response, .ASMX web methods don't need to explicitly serialize because the [System.Web.Script.Services.ScriptService] attribute causes ASP.Net web services to automagically return JSON instead of XML when the jQuery dataType is set to json.
$(document).ready(function () {
$(function () {
$("#<%=Company.ClientID%>").autocomplete({
source: function (request, response) {
$.ajax({
error: function (x, y, z) { alert(x + '\n' + y + '\n' + z); },
type: "GET",
contentType: "application/jsonp; charset=utf-8",
url: "http://www.EzPL8.com/Demos/JSONP.ashx",
dataType: "jsonp",
data: $.toJSON({ term: request.term, maxRows: 12 }),
contentType: "application/jsonp; charset=utf-8",
async: true,
cache: false,
success: function (data) {
response($.map(data, function (item) {
return {
label: item.label,
value: item.value
}
}))
}
})
},
minLength: 2,
select: function (event, ui) {
$("#<%=CoID.ClientID%>").val(ui.item ? ui.item.value : "");
$("#<%=Company.ClientID%>").val(ui.item.label ? ui.item.label : "");
return false;
},
open: function () {
$(this).removeClass("ui-corner-all").addClass("ui-corner-top");
},
close: function () {
$(this).removeClass("ui-corner-top").addClass("ui-corner-all");
},
focus: function (event, ui) {
$("#<%=Company.ClientID%>").val(ui.item.label);
return false;
},
change: function (event, ui) {
$("#<%=CoID.ClientID%>").val(ui.item ? ui.item.value : "");
}
});
});
});
Voila! In the .ASHX code shown below, LINQ-to-SQL queries the database based on the search term defined in the
CompaniesQuery class and returns the primary key and Company name as defined in the Company class. LINQ is pretty cool in that we don't need to write any foreach statements or conversion into ILIST, LIST, or dictionaries. The LINQ object, q, is directly serializable. If you add the skip parameter this jsonp service can do paging for large data sets.
<%@ WebHandler Language="C#" Class="JSONP" %>
using System;
using System.Linq;
using System.Web;
using System.Web.Script.Serialization;
public class JSONP : IHttpHandler
{
JavaScriptSerializer jsSerializer = new JavaScriptSerializer();
string callBackFunction = string.Empty;
string jsonpRequestData = string.Empty;
string jsonpResponse = string.Empty;
int maxRows = 100;
public void ProcessRequest(HttpContext context)
{
if (context.Request.Params.Get(1) != null)
callBackFunction = context.Request.Params.Get(0).ToString();
if (context.Request.Params.Get(1) != null)
jsonpRequestData = context.Request.Params.Get(1).ToString();
CompaniesQuery cq = new CompaniesQuery();
cq = jsSerializer.Deserialize<CompaniesQuery>(jsonpRequestData);
if (cq.maxRows != null)
maxRows = (int)cq.maxRows;
using (EzPL8DataContext db = new EzPL8DataContext())
{
var q = (from c in db.ezpl8_Companies
orderby c.Company
where c.Company.Contains(cq.term) || (cq.term == null)
select new Company { value = c.CoID, label = c.Company }).Take(maxRows).ToList();
jsonpResponse = jsSerializer.Serialize(q);
}
string strOutput = string.Format("{0}({1});", context.Request["callback"], jsonpResponse);
context.Response.AddHeader("Content-Length", strOutput.Length.ToString());
context.Response.ContentType = "application/json";
context.Response.Write(strOutput);
}
public bool IsReusable
{
get
{
return false;
}
}
}
public class Company
{
int _value;
string _label;
public int value
{
get { return _value; }
set { _value = value; }
}
public string label
{
get { return _label; }
set { _label = value; }
}
}
public class CompaniesQuery
{
int? _maxRows;
string _term;
public int? maxRows
{
get { return _maxRows; }
set { _maxRows = value; }
}
public string term
{
get { return _term; }
set { _term = value; }
}
}