Expose Your Site Data with Web Services

©2004 - Alex Homer, Stonebroom Limited, England. 

 

The increasing use of XML has prompted many exciting developments in Web site design and development. It has also provided new ways for site authors to expose information to visitors. A prime example is the explosive growth in "blogging" (the use of a Web-based log or diary that is updated regularly). While blogs might turn out to be just a passing fad, like CB radio in the eighties, their use of a standardized XML format does neatly demonstrate the way that information can be exposed without any preconceived style or formatting applied to it.

Choosing a Data Format

RSS is an example of a special-purpose XML format, with the elements and attribute meanings pre-defined by the RSS standards (it is, therefore, and application of XML). This means that it isn’t suitable for generic information types that you might want to expose from your Web site or Web applications. So, if this is the case, what format should you use?

 

The Web browser/Web server combination we use today was designed to deliver HTML, which implicitly contains display information as well as data. But this doesn’t mean that the Web server must deliver HTML. When a page contains an <img> element, or a hyperlink to a ZIP file, the Web server delivers this content in its natural format. OK, so it might have to be UU-encoded for transmission across the Internet, but it certainly isn't HTML when it arrives.

Delivering Simple Text Strings

So, as an example of a non-HTML information delivery, let's assume that our server uses one of the timeserver resources on the Internet to accurately maintain its internal clock. Therefore we can easily deliver the value for the current time on our server to any client by reading the time and generating a response containing this as a simple string. In ASP.NET, for example, we can use:

 

<%@Page Language="VB" %>

<script runat="server">

Sub Page_Load()

  Response.ContentType = "text/html"

  Response.Write(DateTime.Now.ToString("hh:mm:ss"))

End Sub

</script>

 

The data stream returned from the Web Server contains the usual HTTP headers required for communication between server and client, but the content of the returned "page" is just something like 10:25:07. A Web browser will display this, but any other application that references the page will be able to use the text string as required. For example, an executable application written in VB, C++, Java or any other language that is capable of making HTTP requests across the Internet will be able to use the value.

But the World Wants XML!

But simple text strings are not generally the most useful type of data that you might want to expose to the world. Looking at the issues in a more general way, what we really want is a way of delivering information that:

 

  • Has a structure that is defined through some universal standard
  • Can represent complex data types, and not just simple string values
  • Allows the recipient to identify the structure and data types within the content
  • Supports validation of the content for both structure and permitted content

 

All these features have, if fact, long been on the agenda of both the Internet standards bodies (such as the W3C) and those developers and corporations whose life revolves around data interchange. The result today is the evolving standards for Web Services, as supported by most of the major players in the IT world - such as IBM, Microsoft and Sun. Some idea of the huge amount of work that is going on in the Web Services arena can be found at http://www.w3.org/2002/ws/.

Delivering a DataSet

It goes without saying that the obvious choice of format for data exposed by a Web site or application is one that best suits the data itself. Anything much more than a single value, such as a string, requires a data structure that can store multiple items, and so some kind of rowset or recordset seems the ideal solution. The columns can contain items of different data types, such as integers, strings, real numbers, currency values, etc. There can also be one or more rows, providing the required flexibility for exposing all kinds of data.

 

For those of us who live in the Microsoft .NET world, the ideal container for a rowset is the .NET DataSet class. This can contain one or more tables (rowsets), the metadata that defines features such as primary and foreign keys, default values, etc., and the relationships between the tables. The DataSet is also optimized to read and write its content as XML using Microsoft's proprietary diffgram format.

 

We can easily expose a DataSet from a Web site or application using ASP.NET Web Services, and thereby provide the best opportunity for any client to use the data in the way that they choose. And, best of all, clients that don't understand .NET and the DataSet diffgram format can still use the data by treating it as a "normal" XML document.

About ASP.NET Web Services

Because of the hive of activity in the Web Services world, Microsoft chose to include Web Services technology in their release of the .NET Framework a couple of years ago. Web Services are implemented by building a Class file that has the .asmx file extension. The Web server (IIS) maps this to ASP.NET, which compiles and executes the class to expose the functionality it contains to the current request and response objects.

Using HTTP to Access a Web Service

Effectively, this means that you can "call" a Web Service using a normal HTTP request, and get back what is basically just an XML-format string as the response (see Figure 1).

 

Figure 1 - Accessing a Web Service using an HTTP request

Using SOAP to Access a Web Service

Alternatively, if you use a suitable client, you can make a request using Simple Object Access Protocol (SOAP) to the same Web Service. In this case, the client will build a proxy that matches the interface of the Class file exposed by the Web Service, and use this proxy to call the public methods of the Web Service across the network (see Figure 2).

 

Figure 2 - Accessing a Web Service using a SOAP request

When you expose a Web Service from your site or application, ASP.NET automatically builds a page that provides details of the format of the requests that are acceptable for that Web Service, and even allows users to experiment with the Web Service by providing any required parameters and invoking it directly. The options that may be available are SOAP, HTTP GET and HTTP POST. However, for security reasons, one or more of these may be unavailable depending on the security settings defined in the machine.config or web.config file.

Controlling Web Service Access Methods

The three possible access methods for a Web Service are only available if the configuration defined in the machine.config or web.config file permits them to be exposed. For all to be available to both local and remote requests, the configuration file should contain the following <webServices> element:

 

<system.web>

  <webServices>

    <protocols>

      <add name="HttpSoap"/>

      <add name="HttpPost"/>

      <add name="HttpGet"/>

    </protocols>

  </webServices>

</system.web>

 

Alternatively, you can limit access for HTTP POST operations to the local machine only by using the value HttpPostLocalhost, and/or prevent the default service page (which contains the details of the Web Service and the opportunity to invoke it) from being displayed by using the value Description. For example, the following web.config file allows SOAP requests from any client, POST requests from the local machine only, and also removes the default service page:

 

<system.web>

  <webServices>

    <protocols>

      <add name="HttpSoap"/>

      <remove name="HttpPost"/>

      <remove name="HttpGet"/>

      <remove name="Description"/>

      <add name="HttpPostLocalhost"/>

    </protocols>

  </webServices>

</system.web>

An Example - Exposing Site Traffic Data

As an example of exposing data from a Web site, we do just this with our own site at http://www.daveandal.net/. Figure 3 shows the main Web Service page that is available from a link on our site's "traffic data" page. You can see that there are four public methods that the user can access. Each one returns a diffgram that contains the relevant information.

 

Figure 3 - The main Web Service page showing the available public methods

 

There are no user-configurable parameters in this example, but there is no reason why these can't be added if required. Our choice to avoid these was made simply to prevent very large results sets being delivered, and compromising bandwidth over our rather limited bandwidth connection. Later in this article you'll see a example of a parameterized Web Service method.

The Returned Data Formats

Selecting one of the Web Service methods shown in Figure 3 provides a page that contains a button to invoke that method, as well as a description of the formats required for HTTP and SOAP requests. While the actual format differs between the two, the important point is the description of what is returned from the method. All the methods return a DataSet. For an HTTP request, the returned XML is shown as being:

 

<?xml version="1.0" encoding="utf-8"?>

<DataSet xmlns="http://www.stonebroom.com/webservices/publiciislogs">

  <schema xmlns="http://www.w3.org/2001/XMLSchema">

    ... schema here ...

  </schema>

  ... XML content here ...

</DataSet>

 

Notice that the content is marked as being a DataSet by the root element of the returned content. However, for a SOAP request, the returned XML is shown as being:

 

<?xml version="1.0" encoding="utf-8"?>

<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

               xmlns:xsd="http://www.w3.org/2001/XMLSchema"

               xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">

  <soap:Body>

    <LanguageSummaryResponse

        xmlns="http://www.stonebroom.com/webservices/publiciislogs">

      <LanguageSummaryResult>

        <xsd:schema>

          ... schema here ...

        </xsd:schema>

        ... XML content here ...

      </LanguageSummaryResult >

    </LanguageSummaryResponse>

  </soap:Body>

</soap:Envelope>

 

In this case, the root element is a SOAP envelope, the body of which contains the response from the LanguageSummary method within our Web Service. But the important point to note is that the content of the <DataSet> element and the <LanguageSummaryResponse> elements is the same - the XML schema for the data that is being returned, followed by the data itself as XML that matches the schema. As an example, the next listing shows the results of invoking the LanguageSummary method of our Web service through HTTP (some repeated elements have been removed to improve readability):

 

<?xml version="1.0" encoding="utf-8" ?>

<DataSet xmlns="http://www.stonebroom.com/webservices/publiciislogs">

 <xs:schema id="NewDataSet" xmlns=""

            xmlns:xs="http://www.w3.org/2001/XMLSchema"

            xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">

  <xs:element name="NewDataSet" msdata:IsDataSet="true"

              msdata:Locale="en-GB">

   <xs:complexType>

    <xs:choice maxOccurs="unbounded">

     <xs:element name="CountrySummary">

      <xs:complexType>

       <xs:sequence>

        <xs:element name="Year" type="xs:int" minOccurs="0" />

        <xs:element name="Week" type="xs:int" minOccurs="0" />

        <xs:element name="LangCode" type="xs:string" minOccurs="0" />

        <xs:element name="Language" type="xs:string" minOccurs="0" />

        <xs:element name="Count" type="xs:int" minOccurs="0" />

       </xs:sequence>

      </xs:complexType>

     </xs:element>

    </xs:choice>

   </xs:complexType>

  </xs:element>

 </xs:schema>

 

 <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"

        xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">

  <NewDataSet xmlns="">

   <CountrySummary diffgr:id="CountrySummary1" msdata:rowOrder="0">

    <Year>2004</Year>

    <Week>6</Week>

    <LangCode>en-us</LangCode>

    <Language>English (United States)</Language>

    <Count>1151</Count>

   </CountrySummary>

   <CountrySummary diffgr:id="CountrySummary2" msdata:rowOrder="1">

    <Year>2004</Year>

    <Week>6</Week>

    <LangCode>es</LangCode>

    <Language>Spanish</Language>

    <Count>38</Count>

   </CountrySummary> 

   ... more CountrySummary elements here ...

  </NewDataSet>

 </diffgr:diffgram>

 

</DataSet>

 

The first section within the <DataSet> root element contains the XML schema (the <xs:schema> element and its content). The second section is a <diffgr:diffgram> element that contains the data that was in the DataSet.

 

From this combination of the XML schema and the XML data content, you can clearly see the data itself, and make reasonable assumptions about what it means. The schema defines the data types (the example data contains both integer and string data types), while the element names (the column names in the DataSet) are pretty much self-describing. So, anyone can easily access this Web Service by submitting either a SOAP or HTTP request to our server, be able to make sense of the response, and use the data itself in their own applications.

Building a Web Service

In essence, all you need to do in ASP.NET to create a Web Service is to decorate a Public method of a Class with the appropriate attributes. ASP.NET defines the technique for creating the Class file as requiring the WebService directive and class name to be provided, as shown here:

 

<%@WebService Language="VB" Class="PublicIISLogs"%>

 

After that come any Import statements you require. Unlike ASP.NET Web pages (.aspx files) there are no namespaces imported by default into a Web Service file so you'll need at minimum System, System.Web and System.Web.Services. On top of that are the namespaces for any data access classes you use (in our example we need System.Data and System.Data.SqlClient). And don't forget System.Configuration to allow you to access the contents of web.config to extract a connection string or other information. Other namespaces you might need are System.Collections (if you are using a collection such as an ArrayList), or System.Xml to access XML documents:

 

Imports System

Imports System.Data

Imports System.Data.SqlClient

Imports System.Web

Imports System.Web.Services

Imports System.Configuration

 

Next comes the opening Public Class statement. Here you can see the WebService declaration and attributes, including the rather odd syntax they require in VB.NET. The Description attribute value is shown in the default Web Service page that ASP.NET creates, and the Namespace is something unique to your site and service that can be used to differentiate it from all other Web Services that are accessible on the Internet:

 

<WebService(Description:="DaveAndAl.com Public Traffic Data Service", _

Namespace:="http://www.stonebroom.com/webservices/publiciislogs" _

)> Public Class PublicIISLogs

 

<WebMethod(Description:="Traffic summary for the previous 52 weeks")> _

Public Function TrafficSummary() As DataSet

 

  Dim oDS As New DataSet

  ... code to fill DataSet as required ...

  Return oDS

 

End Function

 

End Class

The WebService attribute added to the Public Class defines the Web Service itself, and then you mark any Public methods within it (any Sub or Function in VB.NET) that you want to expose through the Web Service using the WebMethod attribute - as shown in the previous listing. Again, a Description can be applied.

 

There are also other attributes you can add to the WebMethod declaration; to control caching, ASP.NET session support, buffering and transaction support. Search the .NET SDK for "WebMethodAttribute Members" for more details.

The Example Web Service

The example PublicIISLogs Web Service you've seen in the earlier screenshots exposes four Web methods, including one named TrafficSummary that returns data on the total traffic for our site over the past 52 weeks. This is the code for that method:

 

<WebMethod(Description:="Traffic summary for the previous 52 weeks")> _

Public Function TrafficSummary() As DataSet

  Try

    Dim sTableName As String = "WeekSummary"

    Dim dFromDate As Date = DateTime.Now.AddMonths(-12)

    Dim sSQL As String = GetSQLStatement(sTableName, dFromDate)

    Return GetDataSet(sSQL, sTableName)

  Catch oErr As Exception

    Return GetErrorDataSet(oErr.Message)

  End Try

End Function

Building the SQL Statement

You can see that it uses three other routines within the class file, and all these are marked as Private (and they are not visible as methods of the Web Service as they don't have the WebMethod attribute). The first one builds up the rather complex SQL statement required to extract the rows from our IISLogs database, using the values for the starting date and table name passed into this routine as parameters:

 

Private Function GetSQLStatement(sTableName As String, _

                                 dFromDate As Date) As String

  Dim oCal As New System.Globalization.GregorianCalendar

  Dim iYear As Integer = dFromDate.Year

  Dim iWeek As Integer = oCal.GetWeekOfYear(dFromDate, Nothing, Nothing)  

  sSQL = "SELECT Year = MAX(TYearNumber), Week = MAX(TWeekNumber), " _

         & "Hits = SUM(HitCount), KBytes = SUM(KBytes), " _

         & "Sessions = SUM(Sessions) FROM " & sTableName _

         & " WHERE ((TYearNumber = " & iYear.ToString() _

         & " AND TWeekNumber > " & iWeek.ToString() _

         & ") OR (TYearNumber > " & iYear.ToString() _

         & ")) GROUP BY TYearNumber, TWeekNumber " _

         & "ORDER BY TYearNumber, TWeekNumber"

  Return sSQL

End Function

Creating and Populating the DataSet

The second of the Private methods uses the SQL statement and the table name to populate a DataSet with the required results. The source database connection string comes from an key value named IISLogs within our web.config file:

 

Private Function GetDataSet(sSQL As String, sTableName As String) As DataSet

  Dim sConnect As String = ConfigurationSettings.AppSettings("IISLogs")

  Dim oDS As New DataSet()

  Dim oConn As New SqlConnection(sConnect)

  Dim oDA As New SqlDataAdapter(sSQL, oConn)

  oDA.Fill(oDS, sTableName)

  Return oDS

End Function

Managing Error Situations and Returning Status Information

One of the interesting questions that regularly arise when dealing with a Web Service involves managing errors. It's easy enough to trap an exception in the ASP.NET code, and get details of the error that caused the exception to be raised. But how do we communicate this to the client is a useful way?

 

If we are passing back a simple string, such as the current time (as described at the start of this article), we could just trap the error and send back a string such as: "Sorry, an error occurred", or even something more detailed like: "The remote time server failed to respond".  However, if our client is expecting some other data type then we can’t send back a string.

 

For example, if the Web Service usually returns an integer value, there might be some value (perhaps -1 or 999) that can be used to represent an error. This pre-supposes that the client and server can agree beforehand on what return values are valid, what values are treated as errors, and what error each one represents.

 

But in our example, our clients expect a DataSet, and so this is what we have to send back. Of course, we could always send back the value Nothing (null in C#), to indicate a failure, but this isn't really very expressive or useful. Instead, we chose to create a DataSet containing a single table named Errors, which has a single column named Message. This column will contain the text error message that we want to return to the client. The function in our example Web Service class file named GetErrorDataSet, listed below, is called if any error occurs when the client executes the TrafficSummary method:

 

Private Function GetErrorDataSet(sMessage As String) As DataSet

  Try

    Dim oDT As New DataTable("Errors")

    oDT.Columns.Add("Message", System.Type.GetType("System.String"))

    Dim oDR As DataRow = oDT.NewRow()

    oDR("Message") = sMessage

    oDT.Rows.Add(oDR)

    Dim oDS As New DataSet("Error")

    oDS.Tables.Add(oDT)

    Return oDS

  Catch

    Return Nothing

  End Try

End Function

 

Figure 4 - An error within the Web Service is reported back through the Errors table in the DataSet

Next, having seen how easily we can create a Web Service, it's time to tackle the more complex part of the process - how do we use (or consume) a Web Service from a variety of platforms?

Consuming Web Services

While the previous sections have blithely mentioned sending SOAP or HTTP requests to our server, how is this actually accomplished? Sending an HTTP request is easy - the client just has to send an HTTP GET or POST request to the Web Service, specifying the method they want to execute and any parameters that method requires. The example Web Service shown above takes no parameters, so a request is simply made to:

 

http://www.daveandal.net/traffic/trafficdata.asmx/LanguageSummary.

 

Where the Web Service method does require parameters to be passed to it, these are added to the URL as a query string for GET requests, or placed into the body of a <form> when making a POST request. For example, if the LanguageSummary method required parameters named Year and Week, a suitable GET request would be:

 

http://www.daveandal.net/traffic/trafficdata.asmx/LanguageSummary?Year=2004&Week=6

 

Alternatively, an HTML page containing the following <form> section could be used to create a suitable POST request:

 

<html>

<body>

<form method="post" action="/traffic/trafficdata.asmx/LanguageSummary">

  Year: <input type="text" size="4" name="Year" />

  Week :<input type="text" size="4" name="Week" />

  <input type="submit" value="Go" />

</form>

</body>

</html>

Sending SOAP Requests to a Web Service

Sending a SOAP request to a Web Service is a little more complex, as you have to create the appropriate XML message in SOAP format. The next listing shows the request for the LanguageSummary method with the same two parameters as just described for the HTTP request:

 

<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

               xmlns:xsd="http://www.w3.org/2001/XMLSchema"

               xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">

  <soap:Body>

    <LanguageSummary xmlns="...">

      <Year>2004</Year>

      <Week>6</Week>

    </LanguageSummary>

  </soap:Body>

</soap:Envelope>

 

The request consists of a SOAP envelope, within which is the <soap:Body> element. This contains an element that identifies the method required, and itself contains an element named after each of the required parameters with the parameter values as their content.

 

In general, the easiest way to construct a SOAP request is through an appropriate proxy object created on the client (as shown earlier in Figure 2). There are various ways that you can create this proxy, depending on the technology you are using to build the client.

Examples of Consuming a Web Service

The final sections of this article demonstrate how you can consume a Web Service. We've provided a separate Web Service file named trafficparameters.asmx in the webservices folder of the examples. It implements a single method named TrafficSummaryFromWeekYear, which accepts parameters that define the week and year to start from when returning the data from our database. We'll explore various ways that we can make a request to this Web Service, and use the data that is returned.

Building a Web Service Proxy with WSDL.exe

The .NET Framework provides a tool named WSDL.exe (installed by default in the C:\Program Files\Microsoft.NET\SDK\[version]\Bin\ folder) that you can use to build a suitable proxy class for a Web Service. The syntax and parameters for the tool are complex, and fully described in the .NET SDK (search for "wsdl.exe"). However, for most scenarios, all you need is:

 

wsdl.exe  /language:lang  /out:class-file  URL-or-path-of-WSDL-file

 

The default language if not specified is C#, or you can specify VB (Visual Basic .NET), JS (JScript .NET) or VJS (Visual J#). The /out parameter defines the path and name of the proxy class source file that will be generated, and the final parameter is the URL or physical path to the WSDL file that defines the Web Service. If you are accessing an ASP.NET Web Service, you can get the WSDL from it by appending ?WSDL to the URL as a query string (unless the Web Service has disabled the default service page as discussed earlier in this article).

To make things easier, we've provided a batch file (create-proxy.bat in the create-proxy folder) that executes WSDL.exe with the appropriate parameters for our example files. Note that we specifically select the tool installed with version 1.1 of the Framework in our example:

 

"C:\Program Files\Microsoft.NET\SDK\v1.1\Bin\wsdl.exe" /language:VB /out:IISLogProxy.vb http://../webservices/trafficparameters.asmx?WSDL

 

The final step is to compile the new proxy source class file and place it into the bin folder so that we can access it from an ASP.NET page. The batch file we provide does this as well, creating the compiled assembly named IISLogProxy.dll:

 

C:\WINNT\Microsoft.NET\Framework\v1.1.4322\vbc /out:..\bin\IISLogProxy.dll /t:library /r:System.dll, System.Web.dll, System.Data.dll, System.Xml.dll, System.Web.Services.dll IISLogProxy.vb

Using the Web Service Proxy in ASP.NET

Having created an assembly that implements a proxy for the Web Service, we can easily use this in any other .NET application. The following example (aspnet-proxy-client.aspx in the aspnet-client folder) uses it within an ASP.NET page. The page contains a server-side <form> with two text boxes for the parameter values (the week and year number) and a "submit" button; plus an ASP.NET DataGrid control:

 

<form runat="server">

Starting from week:

<asp:TextBox id="txtWeek" Text="4" Columns="1" runat="server" />

year: <asp:TextBox id="txtYear" Text="2004" Columns="3" runat="server" />

<asp:Button id="btnGo" Text="Go" OnClick="ShowData" runat="server" /><p />

</form>

<asp:DataGrid id="dgr1" runat="server" />

 

The "submit" button simply executes a routine named ShowData. Because the assembly is in the bin folder of the ASP.NET application, we can reference it by importing the namespace (in this case the name of the Public Class within the assembly). Then, within the ShowData routine, we can create a new instance of the proxy class and call its methods.

 

The ShowData routine is listed below. The method we want to call is named TrafficSummaryFromWeekYear, and it takes the start week and year as its two parameters. We get these values from the text boxes on the page, and assign the result (which is a DataSet object) to the ASP.NET DataGrid control to display the contents of the WeekSummary table:

 

<%@Import Namespace="IISLogsParameters" %>

 

Sub ShowData(sender As Object, args As EventArgs)

  Dim oWS As New IISLogsParameters()

  dgr1.DataSource = oWS.TrafficSummaryFromWeekYear(txtWeek.Text, _

                                                   txtYear.Text)

  dgr1.DataMember = "WeekSummary"

  dgr1.DataBind()

End Sub

 

Figure 5 shows the example page in use. It contains extra formatting attributes on the DataGrid to provide the results you see here, but functionally the code is as simple as we've just described.

 

Figure 5 - The example page that accesses a Web Service via a Proxy Client in ASP.NET

Accessing a Web Service with the IE5 Web Service Behavior

The ASP.NET example provides a neat and simple solution to consuming a Web Service, though it only works because ASP.NET understands the objects we've used - an ASP.NET Web Service and a .NET DataSet. Other clients may not have the same level of support for .NET objects, but that doesn’t mean they can't be persuaded to use a Web Service.

 

For example, Internet Explorer has no concept of what .NET is, and the scripting languages it provides (VBScript and JScript) are not managed code languages. However, the IE team at Microsoft have built a script behavior (a HyperText Component or HTC) that can be used to interact with a Web Service from client-side code running within a Web page in IE5 and higher.

The example page we provide (ie5-client.aspx in the ie5-client folder) demonstrates the IE5 Web Service Behavior. The behavior component can be inserted into a page using the standard approach for HTCs, by defining it in a style attribute of an element using the special behaviour:url() selector. In the example page, we attach it to the <span> element that contains the page heading:

 

<span class="heading" id="htcWService" style="behavior:url(webservice.htc)">

  Accessing a Web Service using the IE5 Web Service Behavior

</span><hr />

 

The remainder of the visible part of the page consists of a couple of text boxes for the week and year values, and a "submit" button that we'll use to run the client-side code (as it is not on a <form> it will not cause a postback when clicked). There's also a <span> to show interactive status messages, and a <div> to display the results:

 

Starting from week:

<input type="text" size="1" value="4" name="txtWeek" />

year: <input type="text" size="4" value="2004" name="txtYear" />

<input type="submit" value="Go" onclick="openWebService()" /><p />

 

<span id="lblStatus"></span><p />

<div id="divResult"></div>

Opening a Web Service with the Web Service Behavior HTC

The code required to use the Web Service Behavior is not complex, but can become quite lengthy if you choose to handle errors properly. It also makes sense to load the results of the Web Service method call asynchronously, so that the page can continue to operate (and display status details) as the Web Service is located and accessed - which also requires additional code.

 

Basically, we start by calling the useService method of the component to establish the connection to the Web Service. This downloads the WSDL service definition (you can see that "?WSDL" is appended to the URL as a query string), and establishes a "friendly name" that we use to refer to the Web Service later in our code.

 

htcWService.useService("../webservices/trafficparameters.asmx?WSDL",

                       "TrafficData");

 

Then we can call the method(s) of the Web Service using this "friendly name". In the code below, we collect the values for the week and year from the text boxes on the page, and call the TrafficSummaryFromWeekYear method with these parameters:

 

var sWeek = document.all['txtWeek'].value;

var sYear = document.all['txtYear'].value;

var iCallID = htcWService.TrafficData.callService(dataLoaded,

                   "TrafficSummaryFromWeekYear", sWeek, sYear);

 

The first parameter to the callService method shown in the last line of code above is the name of a delegate or event handler that will be executed once the component has finished downloading the SOAP message that is generated by the Web Service.

 

The next listing shows the delegate function named dataLoaded that we use in our example page. It first checks for an error, and if all is well creates a new instance of the MSXML parser that is provided with IE5 (we're using the free-threaded version here, because we want to perform an XSLT transformation on the content to display it later on):

 

function dataLoaded(oResult) {

  if(oResult.error) {

    // ... display error details ...

  }

  else {

    oXMLData = new ActiveXObject('MSXML2.FreeThreadedDOMDocument');

    oXMLData.onreadystatechange = changeFunction;

    oXMLData.validateOnParse = true;

    oXMLData.async = true;

    oXMLData.loadXML(oResult.raw.xml);

  }

}

 

Again we use asynchronous loading, this time by specifying the delegate named changeFunction to be executed each time the readystate property of the MSXML parser changes. Then we can start loading the parser with the XML document that lies within the SOAP envelope that the Web Service behavior received from the Web Service. We get at the "content" by accessing the raw.xml property of the "result" object that is passed to this function by the Web Service Behavior component. This XML document is, of course, the diffgram that represents our original .NET DataSet.

 

As the MSXML parser initializes, extracts the XML document from the SOAP package, loads it, parses it and validates it, the readystate property of the parser changes at each stage. So the changeFunction is called repeatedly, and it's only when the readystate property reaches the value 4 that the process is complete. At this point we can check for an error, and if all is well call another routine that will display the XML document:

 

function changeFunction() {

  if (oXMLData.readyState == 4) {

    if (oXMLData.parseError.errorCode != 0)

     // ... display error details ...

    else {

      showData();

    }

  }

}

Using the Output Generated by a Web Service in IE5

However, IE5 doesn't know what a DataSet is and so we can't use techniques such as data binding to display it. As far as IE5 is concerned, it's just an XML document with an inline schema, which we've loaded into an instance of the normal MSXML parser. But this does mean that we can use any technique that works with XML documents.

 

The ShowData routine in our example page uses a simple XSLT stylesheet to transform the XML document into an HTML table for display. The transformation process is shown in the next listing, and consists of loading the stylesheet into another instance of MSXML (it must be the free-threaded version for this approach to work), creating an XSLTemplate and Processor instance, and performing the transformation to return a string. The string is then inserted into the <div> element on the page:

 

function showData() {

 

  // create a new parser object instance and load stylesheet

  var oXMLStyle = new ActiveXObject('MSXML2.FreeThreadedDOMDocument');

  oXMLStyle.async = false;

  oXMLStyle.load('style-dataset.xsl');

  if (oXMLStyle.parseError.errorCode != 0) {

    // ... display error message ...

    return;

  }

 

  // create a new XSLTemplate object and set stylesheet

  var oTemplate = new ActiveXObject('MSXML2.XSLTemplate');

  oTemplate.stylesheet = oXMLStyle;

 

  // create a processor and specify the XML parser to use

  var oProc = oTemplate.createProcessor();

  oProc.input = oXMLData;

 

  // perform transformation and display results in the page

  if (oProc.transform() == true)

    divResult.innerHTML = oProc.output;

  else

    // ... display error message ...

}

//-->

</script>

 

The result of the process is shown in Figure 6. You can see that we get much the same as we did when accessing the Web Service through a .NET proxy in an ASP.NET page, though we haven't added quite as garish formatting to the grid this time.

 

Figure 6 - Using the IE5 Web Service Behavior HTC to Access a Web Service

Accessing a Web Service as an XML Document

The previous two examples have demonstrated how we can access a Web Service using SOAP. The first one (the ASP.NET page) used a .NET proxy class complied into an assembly to access the Web Service, and handled the results as a .NET DataSet. The second example used a custom component (the Web Service Behavior HTC) to access the Web Service using SOAP methods, but - because IE5 doesn't understand the .NET DataSet - we had to handle the resulting "content" as an XML document.

 

In this next example (aspnet-xml-client.aspx in the aspnet-client folder), we use a technique that doesn't rely on SOAP to access the Web Service, or on the use of a .NET DataSet object to handle the results. We're using an ASP.NET page to access the Web Service, but we do so using the HTTP GET protocol rather than SOAP. Then we handle the results as an XML document, and not as a DataSet. The page contains basically the same controls as the previous ASP.NET example, except that now we have an ASP.NET Xml server control instead of a DataGrid control:

 

<form runat="server">

Starting from week:

<asp:TextBox id="txtWeek" Text="4" Columns="1" runat="server" />

year: <asp:TextBox id="txtYear" Text="2004" Columns="3" runat="server" />

<asp:Button id="btnGo" Text="Go" OnClick="ShowData" runat="server" /><p />

</form>

 

<asp:Xml id="xmlResult" runat="server" />

 

When the button is clicked, we build up a string that is the URL of the Web Service we want to use. As we saw earlier in this article, the URL we use contains the method name and the parameter values, and is of the format:

 

server/path/webservice.asmx/method?param1=value&param2=value&...

 

So, in our example, we specify the trafficparameters.asmx Web Service file, the TrafficSummaryFromWeekYear method, and add as the query string the values extracted from the "week" and "year" text boxes. Then we can create a new XmlDocument instance and load it with the XML that is generated when we make a request to this URL. The XML is, of course, just the schema and data that represents the DataSet - there is no envelope or other containing elements because we making an HTTP GET request this time, and not a SOAP request:

 

<%@Import Namespace="System.Xml" %>

 

Sub ShowData(sender As Object, args As EventArgs)

  Dim sURL As String = "http://localhost/SynchWebService/webservices/" _

    & "trafficparameters.asmx/TrafficSummaryFromWeekYear?Week=" _

    & txtWeek.Text & "&Year=" & txtYear.Text

  Dim oXmlDoc As New XmlDocument()

  oXmlDoc.Load(sURL)

  xmlResult.Document = oXmlDoc

  xmlResult.TransformSource = "style-dataset.xsl"

End Sub

 

Once we've loaded the XML (it would, of course, be a good idea to use a Try...Catch construct here to trap any errors), we can take advantage of the clever ASP.NET Xml server control to display it. Again, even though it represents a .NET DataSet, it is still just a "normal" XML document. The Xml control takes the XML document, plus the XSLT stylesheet we specify as the TransformSource property, and performs the transformation. The result is then inserted into the page that is sent to the client. Figure 7 shows the result. We used the same style sheet as we did in the previous IE5 example, so the output generated is the same HTML table. 

 

Figure 7 - Accessing the Output of a Web Service as an XML Document

Accessing Web Services from Visual Studio, Office and VBA

We've shown you three of the ways that you can consume ASP.NET Web Services, but this is by no means a comprehensive list. For example, you can write your own custom proxy objects, or just stream back the XML from the Web Service and use custom string-handling techniques to extract the values you need.

 

However, other scenarios that are supported in the Microsoft world for handling Web Services are also worth investigating. In the next part of this article, we'll look at how we can leverage Web Services from Visual Studio .NET - in particular for synchronizing data across separate database servers.

 

Other areas where you might like to experiment with Web Services are in Microsoft Office. There is a utility called the Web Service References Tool available for use in Visual Basic for Applications (VBA), which allows any application that hosts VBA to use Web Services. More details can be found at http://msdn.microsoft.com/library/en-us/dnxpwst/html/odc_ofwsrt.asp.

 

There is also a Web Services Toolkit available specifically for use with Microsoft Office 2003 and Microsoft Office XP. For more details of these see:

http://msdn.microsoft.com/library/en-us/odc_2003_ta/html/ODC_landWST2003_ta.asp

http://msdn.microsoft.com/library/en-us/dnxpwst/html/odc_wstoolkitoverview.asp

Summary

In this, the first of two articles, we've looked at how ASP.NET Web Services can be used to expose data from a Web site or Web application in a format that is generally useful - irrespective of the type of client that your are aiming to support.

 

By exposing a .NET DataSet object, you get the best of all worlds. The DataSet is exposed as a diffgram that can be used to fully reconstruct the DataSet on a client that understands .NET. However, because a diffgram is basically just an XML document with an inline schema, clients that have no knowledge of (or support for) .NET can also consume it.

 

After discussing Web Services in broad terms, and seeing how easy they are to create, we looked at the three protocols they support (SOAP, HTTP POST and HTTP GET) in more detail to get an understanding of how we can use them to best effect. We also looked at how we can control access to individual protocols, and even block the standard description page and WSDL generation.

 

Then we demonstrated a simple Web service that accepts parameters, and showed how to access this Web service in a variety of ways. We built a proxy class as a .NET assembly using the WSDL.exe tool, and accessed it from an ASP.NET page to get back a .NET DataSet. We also showed how we can use the IE5 Web Service Behavior to interact with a Web Service using SOAP, but handle the results as an XML document. And as a third example, we accessed the same Web Service using the HTTP GET protocol, and again handled the result as an XML document.

 

The next part of this article develops on the techniques for using Web Services that we’ve seen here, but concentrates on one specific task - synchronizing traffic data across multiple separate Web sites. The same techniques could be used for most other similar tasks as well, of course, such as synchronizing indexes for site searching or accumulating sales data in one place. We'll also be demonstrating how we can use Visual Studio .NET to interact with a Web Service.

 

Read Part 2

 

©2004 - Alex Homer, Stonebroom Limited, England.