ASP.NET allows you to build and compile a Web service directly into an ASP.NET Web application. Alternatively, you can build a stand-alone Web service (in its own project). We take the second approach because it provides a more realistic approach. Web services operate remotely, so it does not make sense to compile them together with a client application, even for demonstration purposes. ASP.NET lets you easily set a reference to the Web service from a client application, such as an ASP.NET Web application.
The best way to continue our discussion on Web services is to jump right in and show how they are built and consumed. The sample code that accompanies this chapter contains two projects: a Web service application and a consumer Web application:
WebService6A : A Web service application for Northwind.asmx , which provides the following Web methods :
GetEmployeeSales() : Executes the [Employee Sales By Country] stored procedure in the Northwind database. It accepts beginning and ending dates.
ListMostExpensiveProducts() : Executes the [Ten Most Expensive Products] stored procedure. It accepts a product count, specifying how many records should be returned.
GetCustomerList() : Executes a custom stored procedure called [CustomerList] , which returns the full listing of customers in the Northwind database.
GetCustomerOrders() : Executes the [CustOrderHist] stored procedure, which returns all orders for a specific customer. It accepts a customer ID.
AspNetChap6 : A client application for the Northwind.asmx Web service:
ap_WSConsumer1.aspx : The client page for the GetEmployeeSales() Web method. This page calls the Web method from server-side code.
ap_WSAsynchCustomer1.aspx : A client page that displays a list of customers and their associated orders using both the GetCustomerList() and GetCustomerOrders() web methods. This page demonstrates how to call Web methods asynchronously.
ap_ExpensiveProducts.aspx : The client page for the ListMostExpensiveProducts() Web method. This page calls the Web method from client-side code.
Each of the three consumer pages represents a different way of accessing Web service methods. The Web service code on its own is fairly simple, but the picture gets more interesting when you look at how the methods get consumed. In summary, we look at three types of Web service consumption:
Synchronous server side: Synchronous access from a code assembly
Asynchronous server side: Asynchronous access from a code assembly
Asynchronous client side: Asynchronous access using the WebService DHTML behavior
Let's look at each client application in turn and discuss the associated Web service methods while working through the clients . We hope you will find this approach to be effective and intuitive because the concepts will be presented by example and will be grounded in useful code that you can apply to real-world challenges.
Figure 6-4 shows the ap_WSConsumer1.aspx client page.
The page accepts beginning and ending dates, and it returns a listing of employee sales by country for the specified date range. The Run Query button posts the form to the server, which in turn calls the GetEmployeeSales() Web method. This method returns a DataSet that gets bound directly to the DataGrid control on the client page.
Listing 6-1 shows the code for the GetEmployeeSales() Web method.
![]() |
Imports System.Data Imports System.Data.SqlClient Imports System.Configuration Imports System.Xml Imports System.Web.Services Imports System.Web.Services.Protocols <WebService(Namespace:=" http://tempuri.org/")> _ Public Class Northwind Inherits System.Web.Services.WebService <WebMethod()> Public Function GetEmployeeSales(ByVal BeginningDate As Date, _ ByVal EndingDate As Date) As DataSet Dim sqlDS As DataSet Dim strConn As String Dim arrParams() As String Dim objDB As Apress.Database Try ' Step 1: Retrieve the connection string from Web.config strConn = ConfigurationSettings.AppSettings("ConnectionString") ' Step 2: Instance a new Database object objDB = New Apress.Database(strConn) ' Step 3: Execute [Employee Sales By Country] arrParams = New String() {"@Beginning_Date", BeginningDate, _ "@Ending_Date", EndingDate} sqlDS = objDB.RunQueryReturnDS("[Employee Sales By Country]", _ arrParams) Catch err As Exception Throw err Finally End Try Return (sqlDS) ' Return the DataSet End Function End Class
![]() |
The following points are interesting to note:
The Web service is assigned to the http://tempuri.org namespace by default.
The code-behind file for this Web service is encapsulated by the Northwind class, which in turn inherits from the System.Web.Services class.
The Web method retrieves its database connection string from the local Web.config file.
The Web method uses a custom database access class called Apress.Database, which you can review in the sample project.
Aside from the Web method attributes and some of the imported classes, Listing 6-1 looks unremarkable.
The client application must set a Web reference to the Web service before any Web methods can be invoked. The steps for setting a Web reference are as follows :
Compile the WebService6A Web service project.
Switch over to the AspNetChap6 project. In Solution Explorer, right-click the project file and select Add Web Reference from the pop-up menu.
In the dialog box, type in the Uniform Resource Indicator (URI) for the Web service: http://localhost/WebService6A/wsNorthwind.asmx .
The contract details will appear in the left pane of the dialog box (as shown in Figure 6-5).
Figure 6-5: Setting a Web reference to a Web service
Click the Add Reference button.
In Solution Explorer, click the localhost server node in the Web References folder to open its property page.
Change the URL behavior property value from "Static" to "Dynamic." This step automatically adds the URI to the Web.config file, which makes it easy for you to update the Web service location should it change in the future.
This URI gets added to a new application setting in the Web.config file:
<appSettings> <add key="AspNetChap6.localhost.Northwind" value="http://localhost/WebService6/wsNorthwind.asmx"/> </appSettings>
The client page can now call the GetEmployeeSales() Web service method as shown in Listing 6-2.
![]() |
Private Sub btnRunQuery_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnRunQuery.Click ' Purpose: Load a Sales Query DataSet using the beginning and ending dates ' provided by the user Dim sqlDS As DataSet Dim objWS As localhost.Northwind Try ' Step 1: Generate the DataSet, using a Web service call objWS = New localhost.Northwind() objWS.Url = _ ConfigurationSettings.AppSettings("AspNetChap6.localhost.Northwind") sqlDS = objWS.GetEmployeeSales(Me.ap_txt_beginning_date.Text, _ Me.ap_txt_ending_date.Text) ' Step 2: Bind the DataSet to the DataGrid BindDataGrid(sqlDS) Catch errSoap As System.Web.Services.Protocols.SoapException Response.Write("SOAP Exception: " & errSoap.Message) Catch err As Exception Response.Write(err.Message) Finally sqlDS = Nothing End Try End Sub
![]() |
In general, you have three main options when it comes to raising exceptions from a Web service method:
Return exceptions: You can build exception messages into the return argument. For example, if your Web service returns an array of strings, then you could have the first array element indicate the success or failure of the call.
Implied exceptions: You can imply that an exception occurred by returning empty or null values. For example, if your Web service returns a DataSet, then you could intentionally return "Nothing" if an exception occurs. This approach is appropriate if the client does not need to know details of why the exception occurred.
SoapException exceptions: The SoapException class is a specialized exception class that is generated by a Web service method when it throws an exception back to the calling client. The client must be able to interpret a SOAP response to interpret the SOAP exception.
The first two options are both acceptable approaches for exception handling. But the third option, using the SoapException class, is the only good choice when the client needs to receive meaningful exception information from the Web service method. Let's explore this approach in further detail.
The SoapException class inherits from the standard System.Exception class, but the similarity ends there. The SoapException class is tailored toward delivering exception messages inside of a SOAP Response envelope. As such, it provides an overloaded constructor that optionally accepts an XML document. This feature is useful for many reasons. If a malformed SOAP request causes the exception, then the Web service could return the offending splice of the SOAP request packet back to the user for remediation . Alternatively, the Web service could return exceptions as XML documents using a standard schema. The client could then in turn transform the exception document into a standard, stylized exception Web page. Table 6-3 lists the properties used in constructing a SoapException class.
PROPERTY | DESCRIPTION |
---|---|
Message | [Required] The Message property describes the exception. |
Code | [Required] SOAP fault codes that indicate what type of exception has occurred. The values are as follows: ClientFaultCode : The client call was malformed, could not be authenticated, or did not contain sufficient information. This fault code indicates that the client's SOAP request must be remedied. ServerFaultCode : The server encountered an exception while processing the client request, but it was not because of any problems with the client's SOAP request. If the code raises a standard exception, then it is automatically converted to a SoapException, and the default setting for Code is ServerFaultCode . VersionMismatchFaultCode : An invalid namespace was found somewhere in the SOAP envelope and must be remedied. MustUnderstandFaultCode : Indicates that every SOAP element must be successfully processed . The Web service may successfully process a request even if certain SOAP elements have minor issues. However, no issues will be allowed if MustUnderstandFaultCode is set. |
Actor | [Optional] The URI of the Web service method that experienced the exception. |
Detail | [Optional] An XML node that represents application-specific exception information. The information here should refer to exceptions that are related to processing the body of the SOAP request. Issues with the SOAP headers should be raised using the SoapHeaderException class. |
InnerException | [Optional] An Exception class that references to the root cause of the Web method exception. |
The GetEmployeeSales() Web service method, as it is currently written, will throw back a standard exception, but it will not return meaningful information about problems with the SOAP request. The Web service automatically converts a standard thrown exception to a SoapException class instance. The client does not have to trap for the SoapException class specifically . Instead, the client can retrieve some exception details, such as the exception message, by trapping for the standard Exception class. This approach is adequate if your client application does not anticipate having problems with the structure of its SOAP request. But sophisticated Web services should always implement the SoapException class because one cannot anticipate the mistakes that outside users will make in compiling their SOAP requests . You will do them a big favor by returning detailed information about the nature of their exception.
There is really no correct way to raise a SOAP exception; however, it must return Message and Code parameters at a minimum. The power of the SoapException class lies in its flexibility. Let's look at one way of raising a detailed SOAP exception using the GetEmployeeSales() Web method.
You can simulate a server-side exception inside the GetEmployeeSales() Web method by manually raising an exception right before the exception handler:
' Simulate a server-side exception err.Raise(vbObjectError + 512, "wsNorthwind.GenerateDataSet", _ "The SOAP request could not be processed.") Catch err As Exception ' Exception handling code goes here
You can add code inside the Web method's exception handler to build a SoapException:
Catch err As Exception ' Step 1: Build the detail element of the SOAP exception. Dim doc As New System.Xml.XmlDocument() Dim node As System.Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _ SoapException.DetailElementName.Name, _ SoapException.DetailElementName.Namespace) ' Step 2: Add a child detail XML element to the document Dim details As System.Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _ "ErrDetail", "http://tempuri.org/") details.InnerText = "An exception occurred while processing the dates: " & _ BeginningDate & " and " & EndingDate node.AppendChild(details) ' Step 3 Assemble the SoapException class and throw the exception Dim errSoap As New SoapException(err.Message, _ SoapException.ServerFaultCode, "wsNorthwind.asmx", node) Throw errSoap ' Throw the SOAP Exception
On the client application side, you can trap for both the general and the detailed exception messages as follows:
Catch errSoap As System.Web.Services.Protocols.SoapException ' Writes out the general SOAP exception message Response.Write("SOAP Exception1: " & errSoap.Message & "<BR>") ' Writes out the full SOAP exception details (from an XML details node) Response.Write("SOAP Exception2: "& errSoap.Detail.InnerText)
The resulting exception gets posted as follows:
SOAP Exception1: System.Web.Services.Protocols.SoapException: The SOAP request could not be processed SOAP Exception2: An exception occurred while processing the dates: 7/10/1996 and 7/10/1996
Clearly, SOAP exception handling is not a trivial undertaking because there are many possibilities for both creating and consuming SOAP exceptions.
Asynchronous Web method calls are useful for calling methods that require a long time to execute. In synchronous calls, the calling thread must wait until the method call is complete. This becomes a problem if the calling thread must wait around for a long-running method call to complete. Recall that the ASP.NET worker process maintains a thread pool for servicing all application requests. Every thread that gets tied up in waiting for a long-running method to complete is unavailable to participate in the finite thread pool. This has the potential to block the ASP.NET worker process so that new application requests get queued up, rather than serviced immediately.
Asynchronous Web method calls free up the calling thread and return it to the thread pool where it can continue servicing other requests. In asynchronous calls, the HttpContext for the current request does not get released until the call is completed, so the details of the request are preserved. Once the asynchronous call completes, any available thread in the pool can pick the request up again and finish executing it.
In practical terms, asynchronous Web method calls may improve the scalability of your Web application because the ASP.NET worker process can continue to handle new application requests, even while it is handling pending requests.
It is not just other applications that may benefit. The current application instance may require a number of lengthy operations. It may be beneficial to handle some of these operations asynchronously so that other, synchronous operations can start earlier. The net result of this mix of synchronous and asynchronous operations is that the Web application's responsiveness and scalability may improve. This will result in better real and perceived performance for the user.
The previous Web service examples all used synchronous method calls, which are the simpler to code compared to asynchronous method calls. Web services do not require any special compilation options to support asynchronous methods calls. This is because the proxy class is automatically built with methods for asynchronous invocation. A Web method such as GetEmployeeOrders() is compiled as three methods in the proxy class:
GetEmployeeOrders(): For synchronous method invocation
BeginGetEmployeeOrders(): To begin asynchronous method invocation
EndGetEmployeeOrders(): To end asynchronous method invocation
VS .NET creates the proxy class automatically when you add a Web reference to the Web service. When you call the Begin<Method> function, .NET automatically spawns a separate thread and dispatches the method call to this new thread. The main ASP.NET worker thread is then free to continue working on other things.
Asynchronous method invocation used to be a difficult task best left for expert level programmers; but with the release of the .NET Framework, this is no longer the case. The .NET Framework handles the infrastructure for asynchronous programming, which lifts a considerable burden off the developer. The .NET Framework defines a design pattern for asynchronous method invocation that makes this a relatively straightforward task. The Web service proxy classes integrate into the design pattern and do not require any special programming steps.
Figure 6-6 shows the ap_WSAsynchConsumer1.aspx client page.
The client page provides a list of all customers in the Northwind database along with their associated orders. The Customers list is generated from the GetCustomerList() Web service method using a synchronous call. The "Get Orders" buttons load the order details for the currently selected customer using the GetCustomerOrders() Web service method. The left button invokes the Web service method synchronously, and the right button invokes the method asynchronously.
The code listings for the Web service methods are straightforward: They execute stored procedures and return DataSet objects. You need only look at the interface code for the purposes of this discussion:
<WebMethod()> Public Function GetCustomerList() As DataSet End Function <WebMethod()> Public Function GetCustomerOrders(ByVal strCustomerID As String) _ As DataSet End Function
The .NET Framework provides a design pattern for asynchronous method invocation, which you can apply in the ASP.NET client application. Let's look at the code (which includes timestamps for illustration purposes) and then discuss how it works:
Imports System.Threading Private Sub btnRunAsynchQuery_Click(ByVal sender As System.Object, ByVal e As _ System.EventArgs) Handles btnRunAsynchQuery.Click Dim objWS As localhost.Northwind objWS = New localhost.Northwind() objWS.Url = ConfigurationSettings.AppSettings(_ "AspNetChap6.localhost.Northwind") strTime = "1. Asynch Request Begins: " & Now.TimeOfDay.ToString Dim asynchCallback As AsyncCallback = New AsyncCallback(AddressOf _ HandleOrderDetails) Dim asynchResult As IAsyncResult = objWS.BeginGetCustomerOrders(_ Me.cboCustomers.SelectedItem.Value, asynchCallback, objWS) strTime &= "2. Asynch Request ends: " & Now.TimeOfDay.ToString strTime &= "3. Main Thread Sleep Begins: " & Now.TimeOfDay.ToString ' Suspend the main thread for 3 seconds to simulate a long operation ' Note, the asynchronous operation will complete during this time, and the ' main thread will be interrupted when this happens Thread.Sleep(3000) strTime &= " 4. Main Thread Sleep Ends: " & Now.TimeOfDay.ToString 'Response.Write(strTime) ' Uncomment to print execution times to the screen End Sub Private Sub HandleOrderDetails(ByVal asynchResult As IAsyncResult) ' Purpose: Callback function for asynch web method call Dim sqlDS As DataSet Dim objWS As localhost.Northwind = asynchResult.AsyncState Try ' Step 1: Retrieve the DataSet sqlDS = objWS.EndGetCustomerOrders(asynchResult) ' Step 2: Populate the data grid with Customer data With Me.DataGrid1 .DataSource = sqlDS .DataBind() End With Catch err As Exception Response.Write(err.Message) Finally sqlDS = Nothing objWS = Nothing strTime &= " 5. Asynch Result Delivered: " & Now.TimeOfDay.ToString End Try End Sub
The asynchronous method call to GetCustomerOrders() is initiated when the user selects a new customer in the drop-down list and then clicks the "btnRunAsAsynchQuery" button (which triggers its _Click() event handler). The design pattern for asynchronous method invocation works as follows:
The _Click( ) event handler sets a reference to the Web service.
The _Click() event handler defines a callback method ( HandleOrderDetails() ) to respond to the end of the asynchronous method call. The callback method is a type-safe function pointer.
The _Click() event handler calls a special implementation of the Web method for asynchronous method invocation, called BeginGetCustomerOrders() . This method call includes a reference to the callback method, which will trigger when the method call is complete.
The main thread continues executing. The _Click() event handler contains a sleep function that simulates three seconds of activity. It is likely that the asynchronous call will complete during this sleep period.
At some point the asynchronous call completes, which invokes the call-back method, HandleOrderDetails() . This method retrieves the Web service output from a special implementation of the method called EndGetCustomerOrders() .
The DataSet that is retrieved from the Web method call gets bound to the DataGrid, which leaves the main thread to complete execution.
Now let's study the embedded timestamps. If the method invocation had been synchronous, the timestamps would have returned in the following order:
1. Asynch Request Begins: {timestamp} 5. Asynch Result Delivered: {timestamp} 2. Asynch Request ends: {timestamp} 3. Main Thread Sleep Begins: {timestamp} 4. Main Thread Sleep Ends: {timestamp}
Instead, the method invocation is asynchronous, and the timestamps return as follows:
1. Asynch Request Begins: 22:00:43.9156588 2. Asynch Request ends: 22:00:43.9156588 3. Main Thread Sleep Begins: 22:00:43.9156588 5. Asynch Result Delivered: 22:00:43.9356882 4. Main Thread Sleep Ends: 22:00:46.9200688
Of course, some Web service methods are not suitable for asynchronous method invocations. If the resultset is needed before the worker process can continue working, then you will have to call the Web service synchronously.
Traditional interactive Web applications are usually designed to use form POST operations to exchange information between the client and the server. From the client's perspective, this action essentially puts the Web application out of commission for the time it takes to post the form to the server and to receive a response back. The form POST approach may be an effective communication channel for most purposes; however, it can also be a restrictive one. There are times when the client needs to exchange information with the server without having to use a full form POST operation.
Classic ASP addressed this issue using remote scripting , which enabled client-side scripts to send requests directly to the Web server and to receive responses without requiring a form POST operation. But remote scripting proved to be limiting because it supported a very narrow range of primitive data types and had a 2KB maximum on the size of communicated data.
Now that you can build Web services, it is a logical next step to ask if you can invoke Web methods from client-side script. The answer is "yes," and the WebService DHTML behavior provides this capability.
The WebService behavior allows client-side scripts to call methods on remote Web services using SOAP. The behavior is implemented as an HTML Control (HTC) file that can be attached to a Web page and called directly from client-side script. The behavior encapsulates all of the SOAP request and response details, and it supports most of the data types outlined in Table 6-2. The WebService behavior provides a simple method for sending requests to a Web service, and it exposes the generated response as a scriptable result object that provides the following information:
The raw XML of the generated response
Direct access to the response data values
Exception information
The one limitation with the WebService behavior is that it cannot invoke Web services on remote servers ”in other words, on servers that are on a different domain from the Web server that hosts the client-side script. As a workaround you could configure a Web server in your domain to act as a proxy for a remote server. This approach requires complicated configuration and may incur significant performance costs.
Let's look at how to attach the WebService behavior to a page and then use it to call Web service methods.
Note | The WebService behavior may only be used with Microsoft Internet Explorer 5.5 or greater. You can find articles, documentation, and download instructions for the WebService behavior under MSDN Home MSDN Library Behaviors WebService Behavior or at http://msdn.microsoft.com/library/default.asp?url=/workshop/author/webservice/overview.asp . |
The WebService behavior is implemented in a file called WebService.htc , which is available for download at http://msdn.microsoft.com/downloads/samples/internet/behaviors/library/webservice/default.asp .
You must include the WebService behavior file in the ASP.NET client application, typically within the same directory as the Web form *.aspx pages. You only need one copy of the file per application instance. Once the file is installed, you can use it to call a Web service method as follows:
Attach the WebService behavior to a Web page.
Identify the Web service you want to use.
Call a method on the Web service.
Handle the results of the method call in a callback function.
Let's show each of these steps by example, using a page in the sample client project.
The AspNetChap6 sample project contains a client page called ap_ExpensiveProducts.aspx , which illustrates how to call a Web service from client-side script. The following code listings come from the ap_ExpensiveProducts.aspx page.
Before proceeding, let's take a quick look at the client-side page, as shown in Figure 6-7.
The page simply queries the Northwind database for up to 10 of the most expensive products every time the Run Query button is clicked. Listing 6-3 shows an abbreviated version of the ListMostExpensiveProducts() Web method.
![]() |
<WebMethod()> Public Function ListMostExpensiveProducts(ByVal nProductCount _ As Integer) As String() Dim strConn As String Dim objDB As Apress.Database Dim sqlDR As SqlDataReader Dim i As Integer = -1 Dim arrResult(0) As String ' 1-D array Dim nReturnCount As Integer ' Retrieve the connection string from Web.config strConn = ConfigurationSettings.AppSettings("ConnectionString") ' Set nReturnCount between 1 and 10 nReturnCount = IIf(nProductCount > 10, 10, nProductCount) nReturnCount = IIf(nReturnCount = 0, 1, nReturnCount) Try objDB = New Apress.Database(strConn) Dim arrParams() As String = New String() {} ' No arguments sqlDR = objDB.RunQueryReturnDR("[Ten Most Expensive Products]", _ arrParams) While sqlDR.Read() i += 1 'Filter # of returned records, using nReturnCount If i + 1 > nReturnCount Then Exit While ReDim Preserve arrResult(i) arrResult(i) = sqlDR.GetValue(0) End While sqlDR.Close() Catch err As Exception ' Exception handling code goes here Finally End Try Return (arrResult) End Function
![]() |
The "Run Query" button invokes the ListMostExpensiveProducts() Web service method, using the WebService behavior. The products are returned from the Web service as an array of strings. The client page does not repost when the button is clicked, and the results get displayed in a pop-up dialog box over the page.
To attach the WebService behavior to a page, simply add a <div> tag with a behavior style as follows:
<body MS_POSITIONING="GridLayout" onload="Initialize();"> <!-- Attach the web service behavior to a div element --> <div id="service" style="BEHAVIOR: url(webservice.htc)"></div> <!-- End Attach --> <form id="Form1" method="post" runat="server"> <--Form Elements go here --> </form> </body>
This code assumes that the WebService.htc file lives in the same directory as ap_ExpensiveProducts.aspx .
Notice that we call a client-side function called Initialize() in the <body> tag's onLoad() event. This function initializes the behavior as follows:
function Initialize() { // Create an instance of the web service and call it svcNorthwind service.useService("http://localhost/WebService6A/wsNorthwind.asmx?WSDL", _ "svcNorthwind"); }
The WebService behavior provides the useService() method for attaching to a Web service WSDL document and for creating an alias for the Web service instance .
To actually call the Web service method, you need to use the behavior's callService() method. The following code is a JavaScript function called getProductList() that gets called by the Run Query button's onClick() event:
function getProductList() { // Note: This function may be called from any JS event, e.g, onClick or onBlur var iCallID; var txt1 = document.forms[0].ap_txt_product_count; var nProductCount = txt1.value; // Code to invoke the wsNorthwind : ListMostExpensiveProducts() method // Note: onProductListResult() is the callback function for the return result iCallID = service.svcNorthwind.callService(onProductListResult, 'ListMostExpensiveProducts', nProductCount); return false; }
The callService() function requires the following arguments:
Callback function: This optional argument represents the name of a call-back function for handling the result of the method call. If you do not specify a callback function, then the behavior will automatically send the result to a generic function called " onresult ," which you are then required to implement.
Web service method name: This required argument represents the name of the Web service method being called.
Web service method parameters: This required argument represents a comma-delimited array of parameters that are required by the Web service method.
The result object gets passed off to a callback function that implements a standard interface (it must accept the result object). This callback function gets called regardless of the outcome of the Web service request. Listing 6-4 shows the custom callback function, called onProductListResult() :
![]() |
function onProductListResult(result) { // Handles the result for wsNorthwind : ListMostExpensiveProducts() var strProductList = ''; if (result.error) { // exception handling code goes here } else { // Assign the result string to a variable var text = result.value; // Loop through the return array n = text.length; for (var r = 0; r < n; r+=1) { strProductList = strProductList + text[r] + ' : ' } // Display the data alert("The Most Expensive Products are: " + strProductList); // Display the raw SOAP packet alert(result.raw.xml); } }
![]() |
Listing 6-4 omits the exception handling code, although the function does check the value of the result.error object. The return array of products is exposed directly via the result object's value property as follows (for a product count of two):
The Most Expensive Products are: Cte de Blaye : Thringer Rostbratwurst
The raw XML of the SOAP response is exposed via the result object's raw.xml property:
<soap:Envelope xmlns:soap=http://schemas.xmlsoap.org/soap/envelope/ xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body><ListMostExpensiveProductsResponse xmlns="http://tempuri.org/"><ListMostExpensiveProductsResult> <string>Cte de Blaye</string> <string>Thringer Rostbratwurst</string> </ListMostExpensiveProductsResult></ListMostExpensiveProductsResponse> </soap:Body> </soap:Envelope>
JavaScript is very versatile and will allow you to insert the result data any-where in the page. Client-side Web service method calls are often used to populate linked combo boxes ”in other words, to repopulate one combo box when the selected item changes in the other. Client-side validation moves to the next level with Web services because you can perform sophisticated server-based validation directly from the client. Client-side calls to the server are obviously not as fast as local script functions, so you need to design for this. For example, we typically disable all buttons on a client form until the Web service method call has completely finished. We typically re-enable the buttons at the end of the call-back function.
Finally, it is important to note that the callService() function invokes an asynchronous Web method call by design (it does not execute synchronous Web method calls). This means that the script interpreter will make the callService() function call and then immediately move to the next line in the JavaScript function. The callback function ( onProductListResult(), in this case) will fire whenever the Web method call is complete. So, if you are executing dependent code, such as populating linked combo boxes, then you must make the second call at the end of the callback function and not following the callService() method call.
This important point is perhaps easiest to understand by example. Listing 6-5 is pseudo-script for populating linked DropDownList controls using Web method calls. Combo2 will only populate if Combo1 has first been successfully populated .
![]() |
function Initialize() { // Create an instance of the Web service and call it svcMyWS service.useService("http://localhost/WebService1/wsMyWS.asmx?WSDL", "svcMyWS"); } function FillCombo1() { var iCallID = service.svcMyWS.callService(onFillCombo1Result,'FillCombo1'); } function onFillCombo1Result() { if (result.error) { // exception handling code goes here } else { // Step 1: Populate Combo1 with data from the Web service // Code Goes Here // Step 2: Call FillCombo2() to populate the dependent combo } } function FillCombo2() { // Populate Combo2 based on the currently selected value in Combo1 var intCombo1Value = document.forms[0].iq_cbo1.value; var iCallID = service.svcMyWS.callService(onFillCombo2Result,'FillCombo2', intCombo1Value); } function onFillCombo1Result() { if (result.error) { // exception handling code goes here } else { // Step 1: Populate Combo2 with data from the Web service // Code Goes Here } }
![]() |
Now, turn your attention to the important topic of exception handling using the Web service behavior.
The WebService behavior provides reasonable support for processing SOAP exceptions. The actual exception details are encapsulated in an object called errorDetail, which is undefined if the value of result.error is "False." The errorDetail object provides three properties that expose exception information:
Code: SOAP fault codes that indicate the type of exception that occurred. These are the same fault codes described in Table 6-3. Possible values include Client , Server , VersionMismatch , and MustUnderstand .
Raw: The raw SOAP response packet that was returned from the Web service method.
String: The exception message.
The errorDetail object is easy to use, as shown here:
if (result.error) { // Retrieve exception information from the event.result.errorDetail // properties var xfaultcode = result.errorDetail.code; var xfaultstring = result.errorDetail.string; var xfaultsoap = result.errorDetail.raw.xml; // Add custom code to handle specific exceptions }
This is how a simple exception would be returned back to the client:
[xfaultcode] soap:Server [xfaultstring] System.Web.Services.Protocols.SoapException: Server was unable to process request. System.Exception: The SOAP request could not be processed. [xfaultsoap] <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <soap:Fault> <faultcode>soap:Server</faultcode> <faultstring>System.Web.Services.Protocols.SoapException: Server was unable to process request.>; System.Exception: The SOAP request could not be processed.</faultstring> <detail/> </soap:Fault> </soap:Body> </soap:Envelope>
It is much more difficult to process SOAP exceptions in client-side JavaScript compared to ASP.NET code because you do not have the benefit of the SoapException class. The full SOAP exception does get returned to the client via the errorDetail object's raw.xml property, so with a little perseverance , you can retrieve whatever detailed exception information you need. Our design approach with client-side Web service calls is to use defensive programming to never allow an exception to occur. This means implementing as much validation as possible before actually issuing the method call. It also means ensuring that exception alert dialogs provide just enough information for the user either to research the issue further or to report it, but not enough information to overwhelm them. Of course, defensive programming is not a substitute for server-side exception handling in the Web method itself.