http://dotnetslackers.com/columns/ajax/ASPNETAjaxWebService.aspx
Introduction
In this article, I will answer some of the common questions which most of the developers face while working with ASP.NET Ajax Web Services.
Basic Call
The first thing I would like to cover is the complete signature of the Web Service method call. Certainly, there are many references available including the ASP.NET Ajax Documentation; but none of them highlighted it.
01.
YourWebService.YourWebMethod(parameters, succeededCallback, failedCallback, userContext);
02.
03.
function
succeededCallback(result, userContext, methodName)
04.
{
05.
}
06.
07.
function
failedCallback(exception, userContext, methodName)
08.
{
09.
}
As you can see, you can pass any contextual data when calling a Web Method. This contextual data along with the method name is available in both Success and Failure callbacks. This extra data becomes handy when you use the same callback function for multiple web methods.
Consider the following example: I have a single callback function, which is used to update the different parts of the same page.
01.
DataService.GetCustomers(
'NY'
, onSuccess, onError,
'NY'
);
02.
DataService.GetEmployees(
'TX'
, onSuccess, onError,
'TX'
);
03.
04.
function
onSuccess(result, userContext, methodName)
05.
{
06.
if
(methodName ==
'GetCustomers'
)
07.
{
08.
//Update a section of the UI
09.
}
10.
else
if
(methodName ==
'GetEmployees'
)
11.
{
12.
//Update another section of the UI
13.
}
14.
15.
if
(userContext ==
'NY'
)
16.
{
17.
//Do a specific action if it for New York
18.
}
19.
else
if
(userContext ==
'TX'
)
20.
{
21.
//Do another action for Texas
22.
}
23.
}
24.
25.
function
onError(exception, userContext, methodName)
26.
{
27.
/*
28.
We can also perform different actions like the
29.
succeededCallback handler based upon the methodName and userContext
30.
*/
31.
}
Certainly, you can use the set_defaultSucceededCallback
and set_defaultFailedCallback
instead of specifying the same callback repeatedly. Note that methodName
is the Web Service method name. You can pass anything as the userContext
parameter; but it will never be passed to the web method.
The next issue I would like to cover is handling the timeout of a Web Service method call. In the early days of ASP.NET Ajax, there was a separate callback function to handle the timeout. With the final release, a timeout is handled in the failedCallback
, like so:
01.
function
onError(exception)
02.
{
03.
if
(exception.get_timedOut())
04.
{
05.
//Timeout
06.
}
07.
else
08.
{
09.
//Exception occurred
10.
}
11.
}
You can use the set_timout
of your Web Service class to increase the default timeout value. If you want to increase all of your Web Services timeouts, then use Sys.Net.WebRequestManager.set_defaultTimeout
(). This will also change the default timeout value of all your Ajax operations, including the UpdatePanel and manual invoking of Sys.Net.WebRequest.
Complex Data Type Interchange
In this section, I will show you how to implement some complex data types interchange with Web Services. Let's begin with arrays.
1.
[WebMethod()]
2.
public
string
[] GetNames()
3.
{
4.
return
new
string
[] {
"Bill"
,
"Scott"
,
"Brad"
};
5.
}
The above method is called as follows:
01.
SimpleService.GetNames(
02.
function
(names)
03.
{
04.
var
result =
''
;
05.
06.
for
(
var
i = 0; i < names.length; i++)
07.
{
08.
result += names[i] +
'\n'
;
09.
}
10.
11.
alert(result);
12.
}
13.
);
Now, let's see how to do the opposite: Pass an array to a web method.
01.
[WebMethod()]
02.
public
bool
SendNames(
string
[] names)
03.
{
04.
foreach
(
string
name
in
names)
05.
{
06.
Console.WriteLine(
"{0}"
, name);
07.
}
08.
09.
return
true
;
10.
}
We can pass the names
array like so:
1.
var
names = [
'Bill'
,
'Scott'
,
'Brad'
];
2.
3.
SimpleService.SendNames(names,
4.
function
(result)
5.
{
6.
alert(result);
7.
}
8.
);
Next, we will see how to pass and return a Dictionary object. Some of you might argue that having a Dictionary object in the method signature always returns a serialization exception in regular Web Services. The ASP.NET Ajax framework allows a Dictionary object in the method signature. Consider the following example, which returns the weathers of different cities of Bangladesh:
01.
[WebMethod()]
02.
public
Dictionary<
string
,
float
> GetWeathers()
03.
{
04.
Dictionary<
string
,
float
> result =
new
Dictionary<
string
,
float
>();
05.
06.
result.Add(
"Dhaka"
, 32.2f);
07.
result.Add(
"Chittagong"
, 36.7f);
08.
result.Add(
"Khulna"
, 34.5f);
09.
result.Add(
"Rajshai"
, 35f);
10.
11.
return
result;
12.
}
We can call this method and format the result as follows:
01.
SimpleService.GetWeathers(
02.
function
(weathers)
03.
{
04.
var
result =
''
;
05.
06.
for
(
var
city
in
weathers)
07.
{
08.
result += String.format(
"{0} : {1}\n"
, city, weathers[city]);
09.
}
10.
11.
alert(result);
12.
}
13.
);
Let's see how to pass a dictionary to a web method.
01.
[WebMethod()]
02.
public
bool
SendWeathers(Dictionary<
string
,
float
> weathers)
03.
{
04.
foreach
(KeyValuePair<
string
,
float
> item
in
weathers)
05.
{
06.
Console.WriteLine(
"{0} : {1}"
, item.Key, item.Value);
07.
}
08.
09.
return
true
;
10.
}
We can pass the weathers to the above web methods like the following:
01.
var
weathers =
new
Object();
02.
03.
weathers[
'Dhaka'
] = 32.2;
04.
weathers[
'Chittagong'
] = 36.7;
05.
weathers[
'Khulna'
] = 34.5;
06.
weathers[
'Rajshai'
] = 35;
07.
08.
SimpleService.SendWeathers(weathers,
09.
function
(result)
10.
{
11.
alert(result);
12.
}
13.
);
Serializing a complete Object Graph
In this section we will see how to return/pass a custom class using Web Services. By default, the ASP.NET Ajax Framework only generates top-level classes in the Web Service proxy. Consider the following web method:
01.
[WebMethod()]
02.
public
bool
SaveCustomer(Customer customer)
03.
{
04.
Console.WriteLine(
"{0}"
, customer.Name);
05.
06.
foreach
(Address address
in
customer.Addresses)
07.
{
08.
Console.WriteLine();
09.
Console.WriteLine(
"{0}"
, address.Street);
10.
Console.WriteLine(
"{0}"
, address.City);
11.
Console.WriteLine(
"{0}"
, address.ZipCode);
12.
Console.WriteLine(
"{0}"
, address.State);
13.
Console.WriteLine(
"{0}"
, address.Country);
14.
Console.WriteLine(
"{0}"
, address.Phone);
15.
}
16.
17.
return
true
;
18.
}
If you call this method like the following, you will get a JavaScript error saying that Address
is undefined:
01.
var
customer =
new
Customer();
02.
03.
customer.Name =
'A good customer'
;
04.
customer.Addresses =
new
Array();
05.
06.
var
bussinessAddress =
new
Address();
07.
08.
bussinessAddress.Street =
'A business Street'
;
09.
bussinessAddress.City =
'A business City'
;
10.
bussinessAddress.ZipCode =
'99999'
;
11.
bussinessAddress.State =
'A business State'
;
12.
bussinessAddress.Country =
'A business Country'
;
13.
bussinessAddress.Phone =
'123456789'
;
14.
15.
Array.add(customer.Addresses, bussinessAddress);
16.
17.
var
homeAddress =
new
Address();
18.
19.
homeAddress.Street =
'A home Street'
;
20.
homeAddress.City =
'A home City'
;
21.
homeAddress.ZipCode =
'88888'
;
22.
homeAddress.State =
'A home State'
;
23.
homeAddress.Country =
'A home Country'
;
24.
homeAddress.Phone =
'987654321'
;
25.
26.
Array.add(customer.Addresses, homeAddress);
27.
28.
SimpleService.SaveCustomer(customer,
29.
function
(result)
30.
{
31.
alert(result);
32.
}
33.
);
Since Address
is a child object of the Customer class, the framework does not include it in the client proxy. To resolve this issue, add the GenerateScriptType
attribute either in the web method or in the Web Service class:
1.
[WebMethod()]
2.
[GenerateScriptType(
typeof
(Address))]
3.
public
bool
SaveCustomer(Customer customer)
Now you will be able to use the previously shown JavaScript code without any errors. The same holds true for enumerations. This issue has also been discussed by Dan Wahlin in this article.
Exclude Serialization
By default the ASP.NET Ajax Framework serializes all the public fields and properties of custom classes in the client proxy. Sometimes we want to exclude a few of the public fields/properties of those custom classes. To do that, use the ScriptIgnore
attribute:
01.
public
class
Customer
02.
{
03.
public
int
ID;
04.
public
string
Name;
05.
public
List<Address> Addresses =
new
List>Address>();
06.
07.
[ScriptIgnore()]
08.
public
bool
IsNew
09.
{
10.
get
11.
{
12.
return
(ID < 1);
13.
}
14.
}
15.
}
However, this will not have any effect if your web method response format is set to xml
instead of default json
. In the former case, use the XmlIgnore
attribute like you do for regular Web Services.
Serialize incompatible types
The built-in JavaScriptSerializer
class of the ASP.NET Ajax framework cannot serialize all the .NET types. Consider the following Employee class; if you try to return it from a web method, you will get a circular reference exception.
01.
public
class
Employee
02.
{
03.
public
string
Name;
04.
public
Employee Boss;
05.
public
List<Employee> Manages =
new
List<Employee>();
06.
}
07.
08.
[WebMethod()]
09.
public
Employee GetEmployeeHierarchy()
10.
{
11.
Employee e1 =
new
Employee();
12.
e1.Name =
"I am the super boss"
;
13.
14.
Employee e2 =
new
Employee();
15.
e2.Name =
"I am 1st boss"
;
16.
e2.Boss = e1;
17.
18.
Employee e3 =
new
Employee();
19.
e3.Name =
"I am 2nd boss"
;
20.
e3.Boss = e1;
21.
22.
e1.Manages.AddRange(
new
Employee[] { e2, e3 });
23.
24.
for
(
int
i = 1; i <= 10; i++)
25.
{
26.
Employee e =
new
Employee();
27.
28.
e.Name =
string
.Format(
"Employee #{0}"
, i);
29.
30.
if
((i % 2) == 0)
31.
{
32.
e.Boss = e2;
33.
e2.Manages.Add(e);
34.
}
35.
else
36.
{
37.
e.Boss = e3;
38.
e3.Manages.Add(e);
39.
}
40.
}
41.
42.
return
e1;
43.
}
In situation like these, the JavaScriptConverter
class comes into action. You can write your own custom converters, which transforms the incompatible types to compatible ones for the JavaScriptSerializer
class. JavaScriptConverter
is an abstract class which has the following signature:
01.
public
abstract
class
JavaScriptConverter
02.
{
03.
public
abstract
IEnumerable<Type> SupportedTypes
04.
{
05.
get
;
06.
}
07.
08.
public
abstract
object
Deserialize(IDictionary<
string
,
object
> dictionary,
09.
Type type, JavaScriptSerializer serializer);
10.
11.
public
abstract
IDictionary<
string
,
object
> Serialize(
object
obj,
12.
JavaScriptSerializer serializer);
13.
}
Overriding the SupportedTypes property is mandatory. It instructs the ASP.NET Ajax Framework about the type the converter is responsible for, as in the following example:
1.
public
override
IEnumerable<Type> SupportedTypes
2.
{
3.
get
4.
{
5.
return
new
Type[] {
typeof
(Employee) };
6.
}
7.
}
If you are both accepting and returning the type, you will need to override both the getter and the setter. In our case we are only returning the Employee; thus we need to override the Serialize method and leave the others without any implementation, like so:
01.
public
override
object
Deserialize(IDictionary<
string
,
object
> dictionary, Type type, JavaScriptSerializer serializer)
02.
{
03.
throw
new
Exception(
"The method or operation is not implemented."
);
04.
}
05.
06.
public
override
IDictionary<
string
,
object
> Serialize(
object
obj, JavaScriptSerializer serializer)
07.
{
08.
Employee e = obj
as
Employee;
09.
Dictionary<
string
,
object
> result =
new
Dictionary<
string
,
object
>();
10.
11.
if
(e !=
null
)
12.
{
13.
Dictionary<
string
,
object
> superBoss =
new
Dictionary<
string
,
object
>();
14.
15.
superBoss.Add(
"Name"
, e.Name);
16.
result.Add(e.Name, superBoss);
17.
18.
if
(e.Manages.Count > 0)
19.
{
20.
string
[] names = Array.ConvertAll<Employee,
string
>(
21.
e.Manages.ToArray(),
22.
new
Converter<Employee,
string
>(
23.
delegate
(Employee employee)
24.
{
25.
return
employee.Name;
26.
}
27.
));
28.
29.
superBoss.Add(
"Manages"
, names);
30.
31.
foreach
(Employee subordinate
in
e.Manages)
32.
{
33.
Serialize(subordinate, result);
34.
}
35.
}
36.
}
37.
38.
return
result;
39.
}
The Serialize
method is simply streamlining the hierarchical employee in a dictionary object. To ensure the converter is called when the employee object is serialized you have to register it in the web.config file:
1.
<
jsonSerialization
>
2.
<
converters
>
3.
<
add
name
=
"EmployeeConverter"
type
=
"EmployeeConverter"
/>
4.
</
converters
>
5.
</
jsonSerialization
>
You can have anything for the name
but the type
attribute needs to be mapped to the type of the custom converter. Once you are done you can obtain the following output with few lines of JavaScript code.
Figure 1: Output obtained using a custom ASP.NET Ajax converter
Long Running Web Service Call
In this section I will show you how to display a progress indicator for a long running task, like the one shown in figure 2.
Figure 2: A progress indicator for a long running task
The design is very simple. First, we invoke a web method, which will start the lengthy task. It will create a status object that contains the progress information, which we will store in the ASP.NET cache. Next, we poll the task status by invoking another web method. Finally, we update the progress bar based upon the task progress. Let's take a peek at the Web Service code.
01.
[WebMethod()]
02.
public
void
StartLongTask()
03.
{
04.
string
taskID = Context.User.Identity.Name +
":longTask"
;
05.
06.
TaskStatus status =
new
TaskStatus();
07.
08.
Context.Cache[taskID] = status;
09.
10.
//Assuming this task has 5 steps to complete
11.
int
step = 1;
12.
while
(step <= 5)
13.
{
14.
//Doing a fake delay of 2 seconds for each step to complete
15.
System.Threading.Thread.Sleep(2000);
16.
status.Progress += 20;
17.
step++;
18.
}
19.
20.
Context.Cache.Remove(taskID);
21.
}
22.
23.
[WebMethod()]
24.
public
TaskStatus GetTaskStatus()
25.
{
26.
string
taskID = Context.User.Identity.Name +
":longTask"
;
27.
28.
return
Context.Cache[taskID]
as
TaskStatus;
29.
}
30.
31.
public
class
TaskStatus
32.
{
33.
public
int
Progress;
34.
//It can hold any other info depending upon your scenerio
35.
}
As you can see, we created the task ID based upon the user name to store it in the cache. Then we are doing some fake delay to simulate a real task and increasing the progress. Note that we cannot use the ASP.NET session, as the session access is always sequential. In the GetTaskStatus
method we are simply returning the status from the cache. Now let's examine the client side code.
01.
function
startTask()
02.
{
03.
clearProgress();
04.
$get(
'progressbar'
).style.display =
''
;
05.
$get(
'message'
).innerHTML =
'Processing, Please wait'
;
06.
_btnStart.disabled =
true
;
07.
08.
SimpleService.StartLongTask();
09.
_timerId = setInterval(updateStatus, 2000);
// Poll the status at 2 Second interval
10.
}
11.
12.
function
updateStatus()
13.
{
14.
SimpleService.GetTaskStatus(
15.
function
(status)
16.
{
17.
if
((status ==
null
) || (status.Progress == 100))
18.
{
19.
clearInterval(_timerId);
20.
$get(
'message'
).innerHTML =
'Task Complete'
;
21.
$get(
'progressbar'
).style.display =
'none'
;
22.
_btnStart.disabled =
false
;
23.
}
24.
else
25.
{
26.
updateProgress(status.Progress);
27.
}
28.
}
29.
);
30.
}
We are first doing some UI initialization and starting the lengthy task. Then we create a timer by calling setInterval
, which polls the task status at two seconds interval. Once we get the status, we check whether the task is complete. If it isn't, we update the progress bar based upon its progress.
Soap Header
It is quite common to add AJAX support for our existing SOAP Web Service. Unfortunately the built-in WebServiceProxy
class does not support either the SOAP header or any custom HTTP header. I've developed a custom proxy which has built-in support for both. Consider the following Web Service:
01.
public
UserCredientialHeader Crediential;
02.
03.
[WebMethod()]
04.
[SoapHeader(
"Crediential"
, Direction = SoapHeaderDirection.In)]
05.
public
string
GetSensitiveData()
06.
{
07.
if
((Crediential.Username !=
"dummyUser"
) || (Crediential.Password !=
"xxx"
))
08.
{
09.
throw
new
System.Security.SecurityException(
"You are not allowed to call this metheod."
);
10.
}
11.
12.
return
"This is a sensitive data"
;
13.
}
14.
15.
public
class
UserCredientialHeader : SoapHeader
16.
{
17.
public
string
Username;
18.
public
string
Password;
19.
}
You will be able to call this method with the following code:
01.
var
SoapHeaderService =
new
SoapHeaderService();
02.
03.
SoapHeaderService.set_path(
'/Code/SoapHeaderService.asmx'
);
04.
05.
function
invokeWSSoapHeader()
06.
{
07.
SoapHeaderService.get_Crediential().UserName =
'dummyUser'
;
08.
SoapHeaderService.get_Crediential().Password =
'xxx'
;
09.
10.
SoapHeaderService.GetSensitiveData(
11.
function
(result)
12.
{
13.
alert(result.documentElement
14.
.getElementsByTagName(
'GetSensitiveDataResult'
)[0]
15.
.firstChild.nodeValue);
16.
},
17.
18.
function
(exception)
19.
{
20.
alert(exception.get_message());
21.
}
22.
);
23.
}
Now let see how to pass a custom HTTP header with the new proxy. Consider the following web method:
01.
[WebMethod()]
02.
public
string
GetPlainData()
03.
{
04.
string
value = Context.Request.Headers[
"xxx"
];
05.
06.
if
(
string
.IsNullOrEmpty(header))
07.
{
08.
throw
new
InvalidOperationException(
"Cannot call this method"
);
09.
}
10.
11.
return
"This is a plain data"
;
12.
}
You will be calling the above method with the following code:
01.
SoapHeaderService.get_headers()[
'xxx'
] =
'123'
;
02.
03.
SoapHeaderService.GetPlainData(
04.
function
(result)
05.
{
06.
alert(result.documentElement
07.
.getElementsByTagName(
'GetPlainDataResult'
)[0]
08.
.firstChild.nodeValue);
09.
},
10.
11.
function
(exception)
12.
{
13.
alert(exception.get_message());
14.
}
15.
);
The main issue is that you have to manually write the proxy class for the Web Service. Also, the return type is always XML instead of a JSON object. The following code shows the web proxy for the above Web Service:
01.
function
UserCredientialHeader()
02.
{
03.
this
.UserName =
''
;
04.
this
.Password =
''
;
05.
}
06.
07.
var
SoapHeaderService =
function
()
08.
{
09.
SoapHeaderService.initializeBase(
this
);
10.
this
._crediential =
new
UserCredientialHeader()
11.
}
12.
13.
SoapHeaderService.prototype =
14.
{
15.
get_Crediential :
function
()
16.
{
17.
if
(arguments.length !== 0)
throw
Error.parameterCount();
18.
19.
return
this
._crediential;
20.
},
21.
22.
GetSensitiveData :
function
(succeededCallback, failedCallback, userContext)
23.
{
24.
var
userName =
this
.get_Crediential().UserName;
25.
var
password =
this
.get_Crediential().Password;
26.
27.
var
requestTemplate =
'<?xml version=\"1.0\" encoding=\"utf-8\"?>'
+
28.
'<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" '
+
29.
'xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" '
+
30.
'xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">'
+
31.
'<soap:Header>'
+
32.
'<UserCredientialHeader xmlns=\"http://tempuri.org/\">'
+
33.
'<Username>{0}</Username>'
+
34.
'<Password>{1}</Password>'
+
35.
'</UserCredientialHeader>'
+
36.
'</soap:Header>'
+
37.
'<soap:Body>'
+
38.
'<GetSensitiveData xmlns=\"http://tempuri.org/\" />'
+
39.
'</soap:Body>'
+
40.
'</soap:Envelope>'
;
41.
42.
var
requestXml = String.format(requestTemplate, userName, password);
43.
44.
return
this
._invoke(requestXml,
'GetSensitiveData'
, succeededCallback, failedCallback, userContext);
45.
},
46.
47.
GetPlainData :
function
(succeededCallback, failedCallback, userContext)
48.
{
49.
var
requestXml =
'<?xml version=\"1.0\" encoding=\"utf-8\"?>'
+
50.
'<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" '
+
51.
'xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" '
+
52.
'xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">'
+
53.
'<soap:Body>'
+
54.
'<GetPlainData xmlns=\"http://tempuri.org/\" />'
+
55.
'</soap:Body>'
+
56.
'</soap:Envelope>'
;
57.
58.
return
this
._invoke(requestXml,
'GetSPlainData'
, succeededCallback,
59.
failedCallback, userContext);
60.
}
61.
}
62.
63.
SoapHeaderService.registerClass(
'SoapHeaderService'
, Sys.Net.SoapWebServiceProxy);
You can also use the above proxy for old versions of ASP.NET projects with ASP.NET Ajax Client Library.
Summary
All the above issues are based on feedback from the ASP.NET Ajax WebService Forum. If you experienced any issues that I have missed, please let me know and I will cover it in the future.