ASP.NET Ajax Web Service

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

JavaScriptConverter.jpg

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

ProgressIndicator.jpg

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\" ' +
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. 



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章