Chapter 16. Creating Web Services

The previous chapter provided an overview of web services, which are basically web applications with no user interface. Web services allow an application to make method calls against another application over the Internet as though it were calling a localdll.

There are two broad aspects to web service development: creating the web service and consuming the web service. This chapter covers the creation of web services. Chapter 17 covers the creation of web service client applications, also known as consumers.

Although a web service has no user interface and no visual component, the architecture and files used to create a web service are very similar to those used to create a web page, which are described in detail in Chapter 2 through Chapter 6. Some of these similarities include:

  • Full implementation of the .NET Framework and Common Language Runtime (CLR), including the object-oriented architecture and all the base class libraries, as well as features such as caching, state, and data access

  • Nearly identical file and code structures

  • All source code files in plain text, which can be created in any text editor

  • Full support by Visual Studio .NET, with all its productivity features, including IntelliSense, code completion, and integrated debugging

  • Configurable on a global or application-wide basis using plain text configuration files

That said, web pages and web services are conceptually very different. A web page entails an interface designed for interaction with a person sitting at a web browser. A web service, on the other hand, consists only of methods, some of which are available for remote calls by client applications.

A web service can be coded inline, in a single file with an extension of .asmx . Alternatively, the application logic of the web service can be segregated into a code-behind file, which is the default behavior of Visual Studio .NET. While code-behind is generally preferred, especially for large projects, both methods will be demonstrated in this chapter.

Tip

The rationale for code-behind is that it provides a clean separation between the presentation and programmatic portions of an application. While this is extremely useful in the development of web pages, it is not really relevant to web services. However, since code-behind is the default coding technique for Visual Studio .NET (which offers so many productivity enhancements), code-behind becomes the de facto preferred technique. In addition, code-behind confers a performance advantage over inline code because the code-behind class must be precompiled for web services, while the .asmx file is compiled into a class the first time it is run.

Whether using an inline or code-behind architecture, the .asmx file is the target entered into the browser for testing or referenced by the utilities that create the proxy dll. (Recall from Chapter 15 that the client application actually makes calls to a proxy dll. Creation of this proxy dllwill be described in detail in the next chapter.)

As a first step in understanding how web services work, we will create a simple web service, called StockTicker, using any favorite text editor. In subsequent sections of this chapter, we will create the same web service using Visual Studio .NET.

A Simple StockTicker

The StockTicker web service will expose two web methods:

GetName

Expects a stock symbol as an argument and returns a string containing the name of the stock

GetPrice

Expects a stock symbol as an argument and returns a number containing the current price of the stock

If this web service were an actual production program, the data returned would be fetched from a live database. In order not to confuse web service issues with data access issues, for this example the data will be stored in a two-dimensional array of strings. For a complete discussion of accessing a database, please see Chapter 11 and Chapter 12.

A single file will be created. The VB.NET version will be called vbStockTicker.asmx and is shown in Example 16-1. The C# version will be called csStockTicker.asmx and is shown in Example 16-2.

The .asmx file contains the entire web service inline. It defines a namespace called ProgAspNet, and creates a class called csStockTicker for the C# version, or vbStockTicker for the VB.NET version. The class instantiates and fills an array to contain the stock data, then creates the two WebMethods that comprise the public aspects of the web service.

If you’re familiar with web page code, you may notice in glancing over Example 16-1 and Example 16-2 that the code for a web service is virtually identical to the code in a code-behind page for an equivalent web page. There are some differences, however, which are highlighted in the code examples and are discussed in the sections following the examples.

Example 16-1. StockTicker web service in VB.NET, vbStockTicker.asmx

               <%@ WebService Language="VB" Class="ProgAspNet.vbStockTicker" %>

Option Strict On
Option Explicit On
Imports System
Imports System.Web.Services

namespace ProgAspNet

   public class vbStockTicker
      inherits System.Web.Services.WebService
        '  Construct and fill an array of stock symbols and prices.
        '  Note: the stock prices are as of 7/4/01.
      dim stocks as string(,) = _
      { _
         {"MSFT","Microsoft","70.47"}, _
         {"DELL","Dell Computers","26.91"}, _
         {"HWP","Hewlett Packard","28.40"}, _
         {"YHOO","Yahoo!","19.81"}, _
         {"GE","General Electric","49.51"}, _
         {"IBM","International Business Machine","112.98"}, _
         {"GM","General Motors","64.72"}, _
         {"F","Ford Motor Company","25.05"} _
      }

      dim i as integer

      <WebMethod>public function GetPrice(StockSymbol as string) as Double
      '  Given a stock symbol, return the price.     
         '  Iterate through the array, looking for the symbol.
         for i = 0 to stocks.GetLength(0) - 1
            '  Do a case-insensitive string compare.
            if (String.Compare(StockSymbol, stocks(i,0), true) = 0) then
               return Convert.ToDouble(stocks(i,2))
            end if
         next

         return 0
      End Function

      <WebMethod>public function GetName(StockSymbol as string) as string
      '  Given a stock symbol, return the name.     
         '  Iterate through the array, looking for the symbol.
         for i = 0 to stocks.GetLength(0) - 1
            '  Do a case-insensitive string compare.
            if (String.Compare(StockSymbol, stocks(i,0), true) = 0) then
               return stocks(i,1)
            end if
         next

         return "Symbol not found."
      End Function
   End Class
End namespace

Example 16-2. StockTicker web service in C#, csStockTicker.asmx

               <%@ WebService Language="C#" Class="ProgAspNet.csStockTicker" %>

using System;
using System.Web.Services;

namespace ProgAspNet
{
   public class csStockTicker : System.Web.Services.WebService
   {
        //  Construct and fill an array of stock symbols and prices.
        //  Note: the stock prices are as of 7/4/01.
      string[,] stocks = 
      {
         {"MSFT","Microsoft","70.47"},
         {"DELL","Dell Computers","26.91"},
         {"HWP","Hewlett Packard","28.40"},
         {"YHOO","Yahoo!","19.81"},
         {"GE","General Electric","49.51"},
         {"IBM","International Business Machine","112.98"},
         {"GM","General Motors","64.72"},
         {"F","Ford Motor Company","25.05"}
      };

      [WebMethod]
      public double GetPrice(string StockSymbol)
      //  Given a stock symbol, return the price.     
      {
         //  Iterate through the array, looking for the symbol.
         for (int i = 0; i < stocks.GetLength(0); i++)
         {
            //  Do a case-insensitive string compare.
            if (String.Compare(StockSymbol, stocks[i,0], true) == 0)
               return Convert.ToDouble(stocks[i,2]);
         }
         return 0;
      }

      [WebMethod]
      public string GetName(string StockSymbol)
      //  Given a stock symbol, return the name.     
      {
         //  Iterate through the array, looking for the symbol.
         for (int i = 0; i < stocks.GetLength(0); i++)
         {
            //  Do a case-insensitive string compare.
            if (String.Compare(StockSymbol, stocks[i,0], true) == 0)
               return stocks[i,1];
         }
         return "Symbol not found.";
      }
   }
}

The WebService Directive

The first difference between a web service and a web page is seen in the first line of Example 16-1 and Example 16-2. A normal .aspx file will have a Page directive as its first line, but a web service has a WebService directive, as reproduced here in VB.NET:

<%@ WebService Language="VB" Class="ProgAspNet.vbStockTicker" %>

and here in C#:

<%@ WebService Language="C#" Class="ProgAspNet.csStockTicker" %>

The WebService directive is required of all web services. Like all directives, it has the syntax:

<%@ DirectiveName Attribute="value" %>

where there can be multiple attribute/value pairs. The order of the attribute/value pairs does not matter.

The Language attribute

The WebService directive’s Language attribute specifies the language used in the web service. Legal values include C#, VB, and JS for C#, VB.NET, and JScript.NET, respectively. The value is not case-sensitive.

The Language attribute is not required. If it is missing, the default value is C#.

The Class attribute

The WebService directive’s Class attribute specifies the name of the class implementing the web service. The Class attribute is required. The class specified can reside in the .asmx file or in a separate file, a technique referred to as code-behind.

If the implementing class resides in a separate file, then that file must be compiled and the resulting dll placed in the in subdirectory under the directory where the .asmx file resides. This will be demonstrated shortly.

Notice that in the code listings in Example 16-1 and Example 16-2, a namespace, ProgAspNet, has been defined. In order to specify the implementing class contained in this namespace fully, the namespace is prepended to the class name in the WebService directive.

Tip

Strictly speaking, the namespace containing the WebService class does not need to be prepended to the inherited class name, since the System.Web.Services namespace is referenced with the Imports keyword in VB.NET and the using keyword in C#. The longer syntax is used to clarify the relationships.

Deriving from the WebService Class

In the StockTicker web service in Example 16-1 and Example 16-2, the StockTicker class (the vbStockTicker class for VB.NET and the csStockTicker class for C#) inherits from the WebService class.

Deriving from the WebService class is optional, but it offers several advantages. The principal one is that you gain access to several common ASP.NET objects, including:

Application and Session

These objects allow the application to take advantage of state management. For a complete discussion of state management, see Chapter 6. State as it pertains specifically to web services will be covered in more detail later in this chapter.

User

This object is useful for authenticating the caller of a web service. For a complete discussion of security, see Chapter 19.

Context

This object provides access to all HTTP-specific information about the caller’s request contained in the HttpContext class.

Application State via HttpContext

Web services have access to the Application object, as do all ASP.NET resources, via the HttpContext object.

So, for example, you could modify Example 16-1 and Example 16-2 to add the web methods shown in Example 16-3 (for VB.NET) and Example 16-4 (for C#) to set and retrieve a value in application state.

Example 16-3. Code modification to vbStockTicker.asmx adding application state

<WebMethod>public sub SetStockExchange(Exchange as string)
   Application("exchange") = Exchange
end sub
<WebMethod>public function GetStockExchange(  ) as string
   return Application("exchange").ToString(  )
end function

Example 16-4. Code modification to csStockTicker.asmx adding application state

 [WebMethod]
public void SetStockExchange(string Exchange)
{
   Application["exchange"] = Exchange;
}

[WebMethod]
public string GetStockExchange(  )
{
   return Application["exchange"].ToString(  );
}

You could accomplish the same thing without inheriting from System.Web.Services.WebService by using the HttpContext object, as demonstrated in Example 16-5 and Example 16-6.

Example 16-5. Code modification to vbStockTicker.asmx adding application state without inheriting WebService

Option Strict On
Option Explicit On
Imports System
Imports System.Web
Imports System.Web.Services

namespace ProgAspNet

   public class vbStockTicker
.
.
.
      <WebMethod>public sub SetStockExchange(Exchange as string)
         dim app as HttpApplicationState
         app = HttpContext.Current.Application
         app("exchange") = Exchange
      end sub

      <WebMethod>public function GetStockExchange(  ) as string
         dim app as HttpApplicationState
         app = HttpContext.Current.Application
         return app("exchange").ToString(  )
      end function

Example 16-6. Code modification to csStockTicker.asmx adding application state without inheriting WebService

using System;
using System.Web;
using System.Web.Services;

namespace ProgAspNet
{
   public class csStockTicker
.
.
.
      [WebMethod]
      public void SetStockExchange(string Exchange)
      {
         HttpApplicationState app;
         app = HttpContext.Current.Application;
         app["exchange"] = Exchange;
      }

      [WebMethod]
      public string GetStockExchange(  )
      {
         HttpApplicationState app;
         app = HttpContext.Current.Application;
         return app["exchange"].ToString(  );
      }

Notice that in Example 16-5 and Example 16-6, a reference to System.Web has been added at the top of the listing. Also, the web service class, vbStockTicker or csStockTicker, no longer inherits from the class WebService. Finally, an HttpApplicationState object is declared in order to access the application state.

The main reason you might not want to inherit from WebService is to overcome the limitation imposed by the .NET Framework that a class can only inherit from one other class. It would be very inconvenient to have to inherit from WebService if you also needed to inherit from another class.

The WebMethod Attribute

As explained previously, a web service is defined by a WebService class. It is not necessary for the WebService class to expose all of its methods to consumers of the web service. Each method you do want to expose must:

  • Be declared as public.

  • Have the WebMethod attribute placed before the method declaration. (The WebMethod attribute comes from the WebMethodAttribute class, which is contained in the System.Web.Services namespace.)

As you saw in Example 16-1 and Example 16-2, the syntax for defining a web method is slightly different, depending on the language. In VB.NET, it looks like this:

<WebMethod>public function GetName(StockSymbol as string) as string

and in C# it looks like this:

[WebMethod]
public string GetName(string StockSymbol)

WebMethod properties

The WebMethod attribute has properties that are used to configure the behavior of the specific web method. The syntax, again, is language-dependent.

In VB.NET, the syntax is:

<WebMethod(PropertyName:=value)> _
public function GetName(StockSymbol as string) as string

and in C# the syntax is:

[WebMethod(PropertyName=value)]
public string GetName(string StockSymbol)

PropertyName is a valid property accepted by the WebMethod attribute (these are described below), and value is the value to be assigned to that property. Note the colon (:) in VB.NET (which is standard VB.NET syntax for named arguments), as well as the use of the line continuation character if the combination of the WebMethod property and method/function call stretches to more than one line.

Regardless of the language, if there are multiple WebMethod properties, separate each property/value pair with a comma within a single set of parentheses. So, for example, in VB.NET:

<WebMethod(BufferResponse:=False, Description:="Sample description")>

or in C#:

[WebMethod(BufferResponse=false, Description="Sample description")]

The following sections describe the valid WebMethod properties.

The BufferResponse property

By default, ASP.NET buffers the entire response to a request before sending it from the server to the client. Under most circumstances, this is the optimal behavior. However, if the response is very lengthy, you might want to disable this buffering by setting the WebMethod attribute’s BufferResponse property to false. If set to false, the response will be returned to the client in 16KB chunks. The default value is true.

For VB.NET, the syntax for BufferResponse is:

<WebMethod(BufferResponse:=False)>

and for C#:

[WebMethod(BufferResponse=false)]

The CacheDuration property

Web services, like web pages, can cache the results returned to clients, as is described fully in Chapter 18. If a client makes a request that is identical to a request made recently by another client, then the server will return the response stored in the cache. This can result in a huge performance gain, especially if servicing the request is an expensive operation, such as querying a database or performing a lengthy computation.

It should be emphasized that in order for the cached results to be used, the new request must be identical to the previous request. If the web method has parameters, the parameter values must be identical. So, for example, if the GetPrice web method of the StockTicker web service is called with a value of msft passed in as the stock symbol, that result will be cached separately from a request with a value of dell passed in. If the web method has multiple parameters, all the parameter values must be the same as the previous request for the cached results from that request to be returned.

The CacheDuration property defines how many seconds after the initial request the cached page is sent in response to subsequent requests. Once this period has expired, a new page is sent. CacheDuration is set to 30 seconds as follows for VB.NET:

<WebMethod(CacheDuration:=30)>

and for C#:

[WebMethod(CacheDuration=30)]

The default value for CacheDuration is zero, which disables caching of results.

If the web method is returning data that does not change much -- say a query against a database that is updated once hourly -- then the cache duration can be set to a suitably long value, say 1800 (e.g., 30 minutes). You could even set the cache duration in this case to 3600 (60 minutes) if the process of updating the database also forces the cache to refresh by making a call to the WebMethod after the database is updated.

On the other hand, if the data returned is very dynamic, then you would want to set the cache duration to a very short time, or to disable it altogether. Also, if the web method does not have a relatively finite range of possible parameters, then caching may not be appropriate.

The Description property

The WebMethod attribute’s Description property allows you to attach a descriptive string to a web method. This description will appear on the web service help page when you test the web service in a browser.

Also, the WebMethod description will be made available to the consumer of the web service, as will be seen in Chapter 17. When a representation of the web service is encoded into the SOAP message that is sent out to potential consumers, the WebMethod Description property is included.

The syntax for Description is as follows for VB.NET:

<WebMethod(Description:="Returns the stock price for the input stock symbol.")>

and for C#:

[WebMethod(Description="Returns the stock price for the input stock symbol.")]

The EnableSession property

The WebMethod attribute’s EnableSession property, if set to true, enables session state for the web method. The default value is false. (For a general discussion of session state, see Chapter 6.)

If the EnableSession property is set to true and the web service inherits from the WebService class (see earlier sections for a description of inheriting from the WebService class), the session state collection can be accessed with the WebService.Session property. If the web service does not inherit from the WebService class, then the session state collection can be accessed directly from HttpContext.Current.Session.

As an example, the code in Example 16-7 and Example 16-8 adds a per-session hit counter to the ongoing StockTicker web service example.

Example 16-7. HitCounter WebMethod in VB.NET

<WebMethod(Description:="Number of hits per session.", EnableSession:=true)> _
public function HitCounter(  ) as integer
   if Session("HitCounter") is Nothing then
      Session("HitCounter") = 1
   else
      Session("HitCounter") = CInt(Session("HitCounter")) + 1
   end if

   return CInt(Session("HitCounter"))
end function

Example 16-8. HitCounter WebMethod in C#

 [WebMethod(Description="Number of hits per session.", EnableSession=true)]
public int HitCounter(  )
{
   if (Session["HitCounter"] == null)
   {
      Session["HitCounter"] = 1;
   }
   else
   {
      Session["HitCounter"] = ((int) Session["HitCounter"]) + 1;
   }

   return ((int) Session["HitCounter"]);
}

Enabling session state adds additional overhead to the application. By leaving session state disabled, performance may be improved.

In Example 16-7 and Example 16-8, it would probably be more efficient to use a member variable to maintain the hit counter, rather than session state, since the examples as written entail two reads of the session state and one write, while a member variable would entail only one read and one write. However, session state is often useful as a global variable that can exceed the scope of a member variable.

Session state is implemented via HTTP cookies, so if the transport mechanism is something other than HTTP, say SMTP, then the session state functionality will not be available.

The MessageName property

It is possible to have more than one method or function in your web service class with the same name. They are differentiated by their signature -- the quantity, data type, and order of their parameters. Each unique signature can be called independently. This is called method overloading, and can cause some confusion.

The WebMethod attribute’s MessageName property eliminates confusion caused by overloaded methods. It allows you to assign a unique alias to a method signature. When this method is referred to in SOAP messages, the MessageName will be used, and not the method name.

Consider Example 16-9 and Example 16-10. In both examples, two methods are added to the StockTicker web service, both named GetValue. They differ in that one accepts only a single string parameter, while the other takes both a string and an integer.

Example 16-9. GetValue WebMethods in VB.NET

' WebMethod generates an error
<WebMethod(Description:="Returns the value of the users holdings " & _
                  " in a specified stock symbol.")> _
public Function GetValue(StockSymbol as string) as double
   '  Put code here to get the username of the current user, fetch both 
   '  the current price of the specified StockSymbol and number of shares 
   '  held by the current user, multiply the two together, and return the
   '  result. 
   return 0
end Function

' WebMethod generates an error
<WebMethod(Description:="This method returns the value of a " & _
           "specified number of shares in a specified stock symbol.")> _
public Function GetValue(StockSymbol as string, NumShares as integer) as double
   '  Put code here to get the current price of the specified StockSymbol,
   '   multiply it times NumShares, and return the result.
   return 0
end function

Example 16-10. GetValue WebMethods in C#

// WebMethod generates an error
[WebMethod(Description="T Returns the value of the users holdings " + 
                        " in a specified stock symbol.")]
public double GetValue(string StockSymbol)
{
   /* Put code here to get the username of the current user, fetch both 
      the current price of the specified StockSymbol and number of shares 
      held by the current user, multiply the two together, and return the
      result. 
   */
   return 0;
}

// WebMethod generates an error
[WebMethod(Description="This method returns the value of a " +
             "specified number of shares in a specified stock symbol.")]
public double GetValue(string StockSymbol, int NumShares)
{
   /*  Put code here to get the current price of the specified 
      StockSymbol, multiply it times NumShares, and return the result.
   */
   return 0;
}

If you attempt to test either of these in a browser, it will return an error similar to that shown in Figure 16-1.

Conflicting WebMethod names

Figure 16-1. Conflicting WebMethod names

If you modify the code in Example 16-9 and Example 16-10 by adding the MessageName property, highlighted in Example 16-11 and Example 16-12, then everything compiles nicely.

Example 16-11. GetValue WebMethods with MessageName in VB.NET

<WebMethod(Description:="Returns the value of the users holdings " & _
               "in a specified stock symbol.", _
            MessageName:="GetValuePortfolio")> _
public Function GetValue(StockSymbol as string) as double
   '  Put code here to get the username of the current user, fetch 
   '  both the current price of the specified StockSymbol and number  
   '  of shares held by the current user, multiply the two together, 
   '  and return the result. 
   return 0
end Function

<WebMethod(Description:="Returns the value of a specified number " & _
               "of shares in a specified stock symbol.", _
            MessageName:="GetValueStock")> _
public Function GetValue(StockSymbol as string, NumShares as integer) as double
   '  Put code here to get the current price of the specified StockSymbol,
   '   multiply it times NumShares, and return the result.
   return 0
end function

Example 16-12. GetValue WebMethods with MessageName in C#

 [WebMethod(Description="Returns the value of the users holdings " +
               "in a specified stock symbol.",
         MessageName="GetValuePortfolio")]
public double GetValue(string StockSymbol)
{
   /*  Put code here to get the username of the current user, fetch 
      both the current price of the specified StockSymbol and number  
      of shares held by the current user, multiply the two together, 
      and return the result. 
   */   return 0;
}

[WebMethod(Description="Returns the value of a specified " +
                  "number of shares in a specified stock symbol.",
         MessageName="GetValueStock")]
public double GetValue(string StockSymbol, int NumShares)
{
   /*  Put code here to get the current price of the specified 
      StockSymbol, multiply it times NumShares, and return the 
      result.
   */
   return 0;
}

Now consumers of the web service will call GetValuePortfolio or GetValueStock rather than GetValue.

To see the impact of this change, examine the WSDL, which is the description of the web service used by clients of the web service. You can look at the WSDL by entering the URL for the .asmx file in a browser, followed by ?WSDL. If you do that for Example 16-11 or Example 16-12, then search for the first occurrence of GetValuePortfolio, you will see something like Figure 16-2.

MessageName WSDL

Figure 16-2. MessageName WSDL

You can see that the section defined by the tag:

<operation name="GetValue">

is present twice. However, in the first instance, the method name used within the operation section of the document is GetValuePortfolio, and in the second instance it is GetValueStock.

The TransactionOption Property

ASP.NET web methods can participate in transactions (see Chapter 12 for more details on transactions), but only if the transaction originates in that web method. In other words, the web method can only participate as the root object in a transaction. This means that a consuming application cannot call a web method as part of a transaction and have that web method participate in the transaction. However, if the web method starts its own transaction, which then fails, the calling transaction will also fail.

The WebMethod attribute’s TransactionOption property specifies whether or not a web method should start a transaction. There are five legal values of the property, all contained in the TransactionOption enumeration. However, due to the fact that a web method transaction must be the root object, there are only two different behaviors: either a new transaction is started or it is not.

These values in the TransactionOption enumeration are used throughout the .NET Framework. However, in the case of web services, the first three values produce the same behavior, and the last two values produce the same behavior.

The three values of TransactionOption that do not start a new transaction are:

  • Disabled (the default)

  • NotSupported

  • Supported

The two values that do start a new transaction are:

  • Required

  • RequiresNew

In order to use transactions in a web service, you must take several additional steps:

  1. Add a reference to System.EnterpriseServices.dll .

    In Visual Studio .NET, this is done through the Solution Explorer or the Project/Add Reference... menu item. If using the Solution Explorer, right-click on References and select Add References.... In either case, you get the dialog box shown in Figure 16-3. Click on the desired component in the list, and then click OK.

    Note

    When coding outside of Visual Studio .NET, you must add an Assembly directive pointing to System.EnterpriseServices:

    <%@ assembly name="System.EnterpriseServices" %>
  2. Add the System.EnterpriseServices namespace to the web service. This is done with the Imports statement in VB.NET and the using statement in C#. In VB.NET, this would be:

    Imports System.EnterpriseServices

    and in C#:

    using System.EnterpriseServices;
  3. Add a TransactionOption property with a value of RequiresNew to the WebMethod attribute. (The value of Required will have the same effect.)

    The syntax for TransactionOption is as follows for VB.NET:

    <WebMethod(TransactionOption:=TransactionOption.RequiresNew)>

    and for C#:

    [WebMethod(Description=TransactionOption.RequiresNew)]
Adding a reference to a project in Visual Studio .NET

Figure 16-3. Adding a reference to a project in Visual Studio .NET

If there are no exceptions thrown by the web method, then the transaction will automatically commit unless the SetAbort method is explicitly called. If any exceptions are thrown, the transactionwillautomatically abort.

The WebService Attribute

The WebService attribute (not to be confused with the WebMethod attribute or the WebService directive) allows additional information to be added to a web service. The WebService attribute is optional.

The syntax for a WebService attribute is dependent on the language used. For VB. NET, it is:

<WebService(PropertyName:=value)>public class vbStockTicker(  )

or:

<WebService(PropertyName:=value)> _
public class vbStockTicker(  )

and for C# it is:

[WebService(PropertyName=value)]
public class csStockTicker(  )

PropertyName is a valid property accepted by the WebService attribute (these are described below), and value is the value to be assigned to that property. Note the colon (:) in VB. NET (which is standard VB.NET syntax for named arguments), as well as the use of the line continuation character if the combination of the WebService attribute and the class declaration stretches to more than one line.

If there are multiple WebService properties, separate each property/value pair with a comma within a single set of parenthesis. So for example, in VB. NET:

<WebService (Description:="A stock ticker using VB.NET.", _
         Name:="StockTicker", _
         Namespace:="www.LibertyAssociates.com")> _

or in C#:

[WebService (Description="A stock ticker using C#.",
         Name="StockTicker",
         Namespace="www.LibertyAssociates.com")]

There are three possible properties for a WebService attribute, described in the next three sections.

The Description property

The WebService attribute’s Description property assigns a descriptive message to the web service. As with the WebMethod attribute’s Description property, the WebService description will be displayed in the web service help page when the page is tested in a browser, and also made available in the SOAP message to any potential consumers of the web service.

The Name property

The name of a web service is displayed at the top of a web service help page when the page is tested in a browser. It is also made available to any potential consumers of the service.

By default, the name of a web service is the name of the class implementing the web service. The WebService attribute’s Name property allows the name to be changed. If you glance back at the syntax given in Section 16.1.5, you’ll notice that the two language implementations of the stock ticker web service have different class names, but the code specifies that both will now be seen as StockTicker.

The Namespace property

Each web service has an XML namespace associated with it. An XML namespace allows you to create names in an XML document that are uniquely identified by a Uniform Resource Identifier (URI). The web service is described using a WSDL document, which is defined in XML. It is important that each WebService attribute has a unique XML namespace associated with it to prevent name conflicts.

The default URI of a web service is http://tempuri.org/. Typically, you will define a new namespace using a unique name, such as a firm’s web site. Although the XML namespace often looks like a web site, it does not need to be a valid URL.

In the syntax given in Section 16.1.5, notice that the Namespace property is set to the web site, www.LibertyAssociates.com.

Data Types

ASP.NET web services can use any CLR-supported primitive data type as either a parameter or a return value. Table 16-1 summarizes the valid types.

Table 16-1. CLR-supported primitive data types

VB.NET

C#

Description

Byte

byte

1-byte unsigned integer

Short

short

2-byte signed integer

Integer

int

4-byte signed integer

Long

long

8-byte signed integer

Single

float

4-byte floating point

Double

double

8-byte floating point

Decimal

decimal

16-byte floating point

Boolean

bool

True/False

Char

char

Single Unicode character

String

string

Sequence of Unicode characters

DateTime

DateTime

Represents dates and times

Object

object

Any type

In addition to the primitive data types, you can also use arrays and ArrayLists of the primitive types. Since data is passed between a web service and its clients using XML, whatever is used as either a parameter or return value must be represented in an XML schema, or XSD.

The examples shown so far in this chapter have used simple primitive types, such as strings and numbers, as parameters and return values. You could also use an array of simple types, as in the code shown here in C#:

[WebMethod]
public string[] GetArray(  )
{
   string[] TestArray = {"a","b","c"};
   return TestArray;
}

The main limitation of using arrays, of course, is that you must know the number of elements at design time. If the number of elements is dynamic, then an ArrayList is called for. If an ArrayList is used in the web service, it is converted to an object array when the web service description is created. The client proxy will return an array of objects, which will then have to be converted to an array of strings.

The ArrayList is contained within the System.Collections namespace. To use an ArrayList, you must include the proper reference, with the Imports keyword in VB.NET, as in the following:

Imports System.Collections

or the using keyword in C#:

using System.Collections;

The code in Example 16-13 and Example 16-14 contains a web method called GetList. It takes a string as a parameter. This match string is then compared with all the firm names in the data store (the array defined at the top of the web service class shown in Example 16-1 and Example 16-2), and the web service returns all the firm names that contain the match string anywhere within the firm name.

Example 16-13. GetList WebMethod in VB.NET

<WebMethod(Description:="Returns all the stock symbols whose firm " & _
               "name matches the input string as *str*.")> _
public function GetList(MatchString as string) as ArrayList
   dim a as ArrayList = new ArrayList(  )

   '  Iterate through the array, looking for matching firm names.
   for i = 0 to stocks.GetLength(0) - 1
      '  Search is case sensitive.
      if stocks(i,1).ToUpper().IndexOf(MatchString.ToUpper(  )) >= 0 then
         a.Add(stocks(i,1))
      end if
   next
   a.Sort(  )
   return a
end function

Example 16-14. GetList WebMethod in C#

 [WebMethod(Description="Returns all the stock symbols whose firm " + 
               "matches the input string as *str*.")]
public ArrayList GetList(string MatchString)
{
   ArrayList a = new ArrayList(  );

   //  Iterate through the array, looking for matching firm names.
   for (int i = 0; i < stocks.GetLength(0); i++)
   {
      //  Search is case sensitive.
      if ( stocks[i,1].ToUpper().IndexOf(MatchString.ToUpper(  )) >= 0)
         a.Add(stocks[i,1]);
   }
   a.Sort(  );
   return a;
}

The web method in Example 16-13 and Example 16-14 first instantiates a new ArrayList, then iterates through the store of firms. This time the web method uses the IndexOf method of the String class. This IndexOf method does a case-sensitive search in a string, looking for the match string. If it finds a match, it returns the index of the first occurrence. If it does not find a match, it returns -1. In order to implement a case-insensitive search, the code first converts both the MatchString and the firm name to uppercase.

If the IndexOf method finds a match, the web method adds the firm name to the ArrayList. The firm name is contained in the second field of the array record, i.e., the field with index 1 (remember that array indices are zero-based). After completing the search, the web method then sorts the ArrayList before returning it to the client.

In order to test this, enter the following URL into a browser. For VB.NET, use:

http://localhost/ProgAspNet/vbStockTicker.asmx

and for C# use:

http://localhost/ProgAspNet/csStockTicker.asmx

In either case, you will get a page with each of the web methods as a link, similar to the page shown in Figure 15-2 in Chapter 15. Clicking on GetList will bring up a page for testing the method. If you enter “or”, as shown in Figure 16-4, you will see the results that would be returned to a client, shown in Figure 16-5. Notice that in the test output, Ford comes before General Motors, even though their order is reversed in the input data. That is a result of sorting the ArrayList prior to return.

GetList test page

Figure 16-4. GetList test page

GetList test results

Figure 16-5. GetList test results

Web services can also use user-defined classes and structs as either parameters or return types. The rules to remember are:

  • All the class variables must be primitive data types or arrays of primitive data types.

  • All the class variables must be public.

To demonstrate the use of classes with web services, add the class definitions shown in Example 16-15 and Example 16-16 to the Stock Ticker being built in this chapter.

Example 16-15. Class Definitions in VB.NET

public class Stock
   public StockSymbol as string
   public StockName as string
   public Price as double
   public History(2) as StockHistory
end class

public class StockHistory
   public TradeDate as DateTime
   public Price as double
end class

Example 16-16. Class Definitions in C#

public class Stock
{
   public string StockSymbol;
   public string StockName;
   public double Price;
   public StockHistory[] History = 
         new StockHistory[2];
}

public class StockHistory
{
   public DateTime TradeDate;
   public double Price;
}

The first class definition, Stock, is comprised of two strings, a double, and an array of type StockHistory. The StockHistory class consists of a date, called the TradeDate, and the stock price on that date.

Tip

In a real-world application, you would never design a stock ticker like this. Instead of the Stock class having an array with a fixed number of stock history records, you would probably want to use a collection. You would also store the data in a database, rather than filling an array. That way, the number of history records returned by the web method would be dependent upon the number of records returned from the database query. In the example here, the data is hard-coded in an array in order to focus on the topic of using classes with web services.

The web method shown in Example 16-17 and Example 16-18 uses the Stock class to return stock history data for the stock symbol passed in to it.

Example 16-17. GetHistory WebMethod in VB.NET

<WebMethod(Description:="Returns stock history for " & _
            "the stock symbol specified.")> _
public function GetHistory(StockSymbol as string) as Stock
   dim stock as new Stock

   '  Iterate through the array, looking for the symbol.
   for i = 0 to stocks.GetLength(0) - 1
      '  Do a case-insensitive string compare.
      if (String.Compare(StockSymbol, stocks(i,0), true) = 0) then
         stock.StockSymbol = StockSymbol
         stock.StockName = stocks(i,1)
         stock.Price = Convert.ToDouble(stocks(i,2))

         '  Populate the StockHistory data.
         stock.History(0) = new StockHistory(  )
         stock.History(0).TradeDate = Convert.ToDateTime("5/1/2001")
         stock.History(0).Price = Convert.ToDouble(23.25)

         stock.History(1) = new StockHistory(  )
         stock.History(1).TradeDate = Convert.ToDateTime("6/1/2001")
         stock.History(1).Price = Convert.ToDouble(28.75)

         return stock
      end if
   next
   stock.StockSymbol = StockSymbol
   stock.StockName = "Stock not found."
   return stock
end function

Example 16-18. GetHistory WebMethod in C#

 [WebMethod(Description="Returns stock history for " +
            "the stock symbol specified.")]
public Stock GetHistory(string StockSymbol)
{
   Stock stock = new Stock(  );

   //  Iterate through the array, looking for the symbol.
   for (int i = 0; i < stocks.GetLength(0); i++)
   {
      //  Do a case-insensitive string compare.
      if (String.Compare(StockSymbol, stocks[i,0], true) == 0)
      {
         stock.StockSymbol = StockSymbol;
         stock.StockName = stocks[i,1];
         stock.Price = Convert.ToDouble(stocks[i,2]);

         //  Populate the StockHistory data.
         stock.History[0] = new StockHistory(  );
         stock.History[0].TradeDate = Convert.ToDateTime("5/1/2001");
         stock.History[0].Price = Convert.ToDouble(23.25);

         stock.History[1] = new StockHistory(  );
         stock.History[1].TradeDate = Convert.ToDateTime("6/1/2001");
         stock.History[1].Price = Convert.ToDouble(28.75);

         return stock;
      }
   }
   stock.StockSymbol = StockSymbol;
   stock.StockName = "Stock not found.";
   return stock;
}

In Example 16-17 and Example 16-18, notice that each class is instantiated before it can be used. Iterating over the array of stocks finds the data to return. The class variables are populated from the array, and then the class itself is returned. If the stock symbol is not found, a message is placed in a convenient field of the stock class and that is returned.

Since a web service can return any data that can be encoded in an XML file, it can also return a DataSet, since that is represented internally as XML by ADO.NET. A DataSet is the only type of ADO.NET data store that can be returned by a web service.

As an exercise, we will modify an example shown previously in Chapter 11 to return a DataSet from the Bugs database used in that chapter.

Tip

Although this sample web method does not really conform to the ongoing Stock Ticker example, we will use it for convenience.

Add the namespaces shown in Example 16-19 and Example 16-20 to the Stock Ticker example.

Example 16-19. Namespace references for DataSet in VB.NET

Imports System.Data
Imports System.Data.SqlClient

Example 16-20. Namespace references for DataSet in C#

using System.Data;
using System.Data.SqlClient;

Now add the web method shown in Example 16-21 (VB.NET) and Example 16-22 (C#). This web method, called GetDataSet, takes no parameters and returns a DataSet object consisting of all the BugIDs and Descriptions from the Bugs database.

Example 16-21. GetDataSet in VB.NET

<WebMethod(Description:="Returns a data set from the Bugs database.")> _
public function GetDataset(  ) as DataSet
   dim connectionString as string
   dim commandString as string
   dim dataAdapter as SqlDataAdapter
   dim dataSet as new DataSet(  )

   ' connect to the Bugs database
   connectionString = "YourServer; uid=sa; 
      pwd=YourPassword; database= ProgASPDotNetBugs "

   ' get records from the Bugs table
   commandString = "Select BugID, Description from Bugs"

   ' create the data set command object and the DataSet
   dataAdapter = new SqlDataAdapter(commandString, connectionString)

   ' fill the data set object
   dataAdapter.Fill(dataSet,"Bugs")

   return dataSet
end function

Example 16-22. GetDataSet in C#

 [WebMethod(Description="Returns a data set from the Bugs database.")]
public DataSet GetDataset(  )
{
   // connect to the Bugs database
   string connectionString = "server=YourServer; uid=sa; 
      pwd= YourPassword; database= ProgASPDotNetBugs ";

   // get records from the Bugs table
   string commandString = "Select BugID, Description from Bugs";

   // create the data set command object and the DataSet
   SqlDataAdapter dataAdapter = new SqlDataAdapter(commandString, connectionString);

   DataSet dataSet = new DataSet(  );

   // fill the data set object
   dataAdapter.Fill(dataSet,"Bugs");

   return dataSet;
}

The code is copied nearly verbatim from the Page_Load method in the code in Example 11-2 and Example 11-3 and was described fully in that chapter. The important thing to note here is that a DataSet object is created from a query, then returned by the web method to the consuming client.

Example 16-23 and Example 16-24 show the completed source code for the example web service that we’ve developed in this chapter up to this point. The code is included here to show how all the snippets of code presented so far fit together.

Example 16-23. vbStockTicker.asmx in VB.NET

<%@ WebService Language="VB" Class="ProgAspNet.vbStockTicker" %>

Option Strict On
Option Explicit On
Imports System
Imports System.Web.Services
Imports System.Collections
Imports System.Data
Imports System.Data.SqlClient

namespace ProgAspNet

   <WebService (Description:="A stock ticker using VB.NET.", _
            Name:="StockTicker", _
            Namespace:="www.LibertyAssociates.com")> _
   public class vbStockTicker
       inherits System.Web.Services.WebService

        '  Construct and fill an array of stock symbols and prices.
        '  Note: the stock prices are as of 7/4/01.
      dim stocks as string(,) = _
      { _
         {"MSFT","Microsoft","70.47"}, _
         {"DELL","Dell Computers","26.91"}, _
         {"HWP","Hewlett Packard","28.40"}, _
         {"YHOO","Yahoo!","19.81"}, _
         {"GE","General Electric","49.51"}, _
         {"IBM","International Business Machine","112.98"}, _
         {"GM","General Motors","64.72"}, _
         {"F","Ford Motor Company","25.05"} _
      }

      dim i as integer

      public class Stock
         public StockSymbol as string
         public StockName as string
         public Price as double
         public History(2) as StockHistory
      end class

      public class StockHistory
         public TradeDate as DateTime
         public Price as double
      end class

      <WebMethod(Description:="Returns stock history for " & _
                  "the stock symbol specified.")> _
      public function GetHistory(StockSymbol as string) as Stock
         dim stock as new Stock

         '  Iterate through the array, looking for the symbol.
         for i = 0 to stocks.GetLength(0) - 1
            '  Do a case-insensitive string compare.
            if (String.Compare(StockSymbol, stocks(i,0), true) = 0) then
               stock.StockSymbol = StockSymbol
               stock.StockName = stocks(i,1)
               stock.Price = Convert.ToDouble(stocks(i,2))

               '  Populate the StockHistory data.
               stock.History(0) = new StockHistory(  )
               stock.History(0).TradeDate = Convert.ToDateTime("5/1/2001")
               stock.History(0).Price = Convert.ToDouble(23.25)

               stock.History(1) = new StockHistory(  )
               stock.History(1).TradeDate = Convert.ToDateTime("6/1/2001")
               stock.History(1).Price = Convert.ToDouble(28.75)

               return stock
            end if
         next
         stock.StockSymbol = StockSymbol
         stock.StockName = "Stock not found."
         return stock
      end function

      <WebMethod(Description:="Returns all the stock symbols whose " & _
                        "firm name matches the input string as *str*.")> _
      public function GetList(MatchString as string) as ArrayList
           dim a as ArrayList = new ArrayList(  )

         '  Iterate through the array, looking for matching firm names.
         for i = 0 to stocks.GetLength(0) - 1
            '  Search is case sensitive.
            if stocks(i,1).ToUpper().IndexOf(MatchString.ToUpper(  )) _
                                 >= 0 then
                   a.Add(stocks(i,1))
            end if
         next
           a.Sort(  )
           return a
      end function

      <WebMethod(Description:="Returns the stock price for the " & _
                        "input stock symbol.", _
               CacheDuration:=20)> _
      public function GetPrice(StockSymbol as string) as Double
      '  Given a stock symbol, return the price.     
         '  Iterate through the array, looking for the symbol.
         for i = 0 to stocks.GetLength(0) - 1
            '  Do a case-insensitive string compare.
            if (String.Compare(StockSymbol, stocks(i,0), true) = 0) then
               return Convert.ToDouble(stocks(i,2))
            end if
         next

         return 0
      End Function

      <WebMethod(Description:="Returns the firm name for the input " & _
                        "stock symbol.", _
               CacheDuration:=86400)> _
      public function GetName(StockSymbol as string) as string
      '  Given a stock symbol, return the name.     
         '  Iterate through the array, looking for the symbol.
         for i = 0 to stocks.GetLength(0) - 1
            '  Do a case-insensitive string compare.
            if (String.Compare(StockSymbol, stocks(i,0), true) = 0) then
               return stocks(i,1)
            end if
         next

         return "Symbol not found."
      End Function

      <WebMethod(Description:="Sets the stock exchange for the " & _
                           "application.")> _
      public sub SetStockExchange(Exchange as string)
         Application("exchange") = Exchange
      end sub

      <WebMethod(Description:="Gets the stock exchange for the " & _
                           "application. It must previously be set.")> _
      public function GetStockExchange(  ) as string
         return Application("exchange").ToString(  )
      end function

      <WebMethod(Description:="Number of hits per session.", _
               EnableSession:=true)> _
      public function HitCounter(  ) as integer
         if Session("HitCounter") is Nothing then
            Session("HitCounter") = 1
         else
            Session("HitCounter") = CInt(Session("HitCounter")) + 1
         end if

         return CInt(Session("HitCounter"))
      end function

      <WebMethod(Description:="Returns the value of the users " & _
                        "holdings in a specified stock symbol.", _
                        MessageName:="GetValuePortfolio")> _
      public Function GetValue(StockSymbol as string) as double
         '  Put code here to get the username of the current user, fetch 
         '  both the current price of the specified StockSymbol and number  
         '  of shares held by the current user, multiply the two together, 
         '  and return the result. 
         return 0
      end Function

      <WebMethod(Description:="Returns the value of a specified " & _
                        "number of shares in a specified stock symbol.", _
                        MessageName:="GetValueStock")> _
      public Function GetValue(StockSymbol as string, _
                        NumShares as integer) as double
         '  Put code here to get the current price of the specified 
         '  StockSymbol,   multiply it times NumShares, and return 
         '  the result.
         return 0
      end function

      <WebMethod(Description:="Returns a data set from the Bugs " & _ 
                        "database.")> _
      public function GetDataset(  ) as DataSet
         dim connectionString as string
         dim commandString as string
         dim dataAdapter as SqlDataAdapter
         dim dataSet as new DataSet(  )

         ' connect to the Bugs database
         connectionString = "server=Ath13; uid=sa; pwd=stersol; " & _
                        "database=Bugs"

         ' get records from the Bugs table
         commandString = "Select BugID, Description from Bugs"

         ' create the data set command object and the DataSet
         dataAdapter = new SqlDataAdapter(commandString, connectionString)

         ' fill the data set object
         dataAdapter.Fill(dataSet,"Bugs")

         return dataSet
      end function

   End Class
End namespace

Example 16-24. csStockTicker.asmx in C#

<%@ WebService Language="C#" Class="ProgAspNet.csStockTicker" %>

using System;
using System.Web.Services;
using System.Collections;
using System.Data;
using System.Data.SqlClient;

namespace ProgAspNet
{
   [WebService (Description="A stock ticker using C#.",
            Name="StockTicker",
            Namespace="www.LibertyAssociates.com")] 
   public class csStockTicker : System.Web.Services.WebService
     {
        //  Construct and fill an array of stock symbols and prices.
        //  Note: the stock prices are as of 7/4/01.
      string[,] stocks = 
      {
         {"MSFT","Microsoft","70.47"},
         {"DELL","Dell Computers","26.91"},
         {"HWP","Hewlett Packard","28.40"},
         {"YHOO","Yahoo!","19.81"},
         {"GE","General Electric","49.51"},
         {"IBM","International Business Machine","112.98"},
         {"GM","General Motors","64.72"},
         {"F","Ford Motor Company","25.05"}
      };

      public class Stock
      {
         public string StockSymbol;
         public string StockName;
         public double Price;
         public StockHistory[] History = 
               new StockHistory[2];
      }

      public class StockHistory
      {
         public DateTime TradeDate;
         public double Price;
      }

      [WebMethod(Description="Returns stock history for " +
                  "the stock symbol specified.")]
      public Stock GetHistory(string StockSymbol)
      {
         Stock stock = new Stock(  );

         //  Iterate through the array, looking for the symbol.
         for (int i = 0; i < stocks.GetLength(0); i++)
         {
            //  Do a case-insensitive string compare.
            if (String.Compare(StockSymbol, stocks[i,0], true) == 0)
            {
               stock.StockSymbol = StockSymbol;
               stock.StockName = stocks[i,1];
               stock.Price = Convert.ToDouble(stocks[i,2]);

               //  Populate the StockHistory data.
               stock.History[0] = new StockHistory(  );
               stock.History[0].TradeDate = 
                    Convert.ToDateTime("5/1/2001");
               stock.History[0].Price = Convert.ToDouble(23.25);

               stock.History[1] = new StockHistory(  );
               stock.History[1].TradeDate = 
                    Convert.ToDateTime("6/1/2001");
               stock.History[1].Price = Convert.ToDouble(28.75);

               return stock;
            }
         }
         stock.StockSymbol = StockSymbol;
         stock.StockName = "Stock not found.";
         return stock;
      }

      [WebMethod(Description="Returns all the stock symbols whose firm " + 
                        "name matches the input string as *str*.")]
      public ArrayList GetList(string MatchString)
      {
           ArrayList a = new ArrayList(  );

         //  Iterate through the array, looking for matching firm names.
         for (int i = 0; i < stocks.GetLength(0); i++)
         {
            //  Search is case sensitive.
            if ( stocks[i,1].ToUpper().IndexOf(MatchString.ToUpper(  )) >= 0)
                     a.Add(stocks[i,1]);
         }
           a.Sort(  );
           return a;
      }

      [WebMethod(Description="Returns the stock price for the input " + 
                        "stock symbol.",   
               CacheDuration=20)]
      public double GetPrice(string StockSymbol)
      //  Given a stock symbol, return the price.     
      {
         //  Iterate through the array, looking for the symbol.
         for (int i = 0; i < stocks.GetLength(0); i++)
         {
            //  Do a case-insensitive string compare.
            if (String.Compare(StockSymbol, stocks[i,0], true) == 0)
               return Convert.ToDouble(stocks[i,2]);
         }
         return 0;
      }

      [WebMethod(Description="Returns the firm name for the input " +
                             "stock symbol.",
               CacheDuration=86400)]
      public string GetName(string StockSymbol)
      //  Given a stock symbol, return the name.     
      {
         //  Iterate through the array, looking for the symbol.
         for (int i = 0; i < stocks.GetLength(0); i++)
         {
            //  Do a case-insensitive string compare.
            if (String.Compare(StockSymbol, stocks[i,0], true) == 0)
               return stocks[i,1];
         }
         return "Symbol not found.";
      }

      [WebMethod(Description="Sets the stock exchange for the " +
                        "application.")]
      public void SetStockExchange(string Exchange)
      {
         Application["exchange"] = Exchange;
      }

      [WebMethod(Description="Gets the stock exchange for the " + 
               "application. It must previously be set.")]
      public string GetStockExchange(  )
      {
         return Application["exchange"].ToString(  );
      }

      [WebMethod(Description="Number of hits per session.", 
               EnableSession=true)]
      public int HitCounter(  )
      {
         if (Session["HitCounter"] == null)
         {
            Session["HitCounter"] = 1;
         }
         else
         {
            Session["HitCounter"] = ((int) Session["HitCounter"]) + 1;
         }

         return ((int) Session["HitCounter"]);
      }

      [WebMethod(Description="Returns the value of the users holdings " +
                        "in a specified stock symbol.",
               MessageName="GetValuePortfolio")]
      public double GetValue(string StockSymbol)
      {
         /*  Put code here to get the username of the current user, fetch 
            both the current price of the specified StockSymbol and number  
            of shares held by the current user, multiply the two together, 
            and return the result. 
         */
         return 0;
      }

      [WebMethod(Description="Returns the value of a specified " +
                        "number of shares in a specified stock symbol.",
               MessageName="GetValueStock")]
      public double GetValue(string StockSymbol, int NumShares)
      {
         /*  Put code here to get the current price of the specified 
            StockSymbol, multiply it times NumShares, and return the 
            result.
         */
         return 0;
      }

      [WebMethod(Description="Returns a data set from the Bugs " +
                        "database.")]
      public DataSet GetDataset(  )
      {
         // connect to the Bugs database
         string connectionString = "server=Ath13; uid=sa; pwd=stersol; " + 
                        "database=Bugs";

         // get records from the Bugs table
         string commandString = "Select BugID, Description from Bugs";

         // create the data set command object and the DataSet
         SqlDataAdapter dataAdapter = new SqlDataAdapter(commandString, 
                              connectionString);

         DataSet dataSet = new DataSet(  );

         // fill the data set object
         dataAdapter.Fill(dataSet,"Bugs");

         return dataSet;
      }
   }
}

Using Code-Behind

When you are creating web pages, code-behind allows you to separate application logic from design or user interface (UI) elements.

Tip

Code-behind is the default code model of Visual Studio .NET. In fact, it is very inconvenient to use Visual Studio .NET to write any ASP.NET application without using code-behind.

Since web services have no design or UI component, the case for using code-behind is not quite so compelling. However, there is a performance benefit to code-behind. As we will discuss, the class implementing the code-behind must be compiled into a dll ahead of time and made available to the web service. By contrast, the WebService class contained in the .asmx file, similar to the page class in the .aspx file, is compiled on the fly by the .NET Framework the first time the class is called. That compiled version is then cached on the server for subsequent requests. For a complete discussion of caching and performance, see Chapter 18. For now, suffice it to say that the first time a web service or web page is called, there will be a delay for inline code while the class is compiled, while a code-behind implementation will never experience that delay.

It is very easy to convert the Stock Ticker web service created so far in this chapter from an inline code model to a code-behind model. In this section, you will first do code-behind in a text editor, then in Visual Studio .NET.

Using a text editor

In a text editor, create a new file called vbStockTickerCodeBehind.asmx for the VB.NET version, and csStockTickerCodeBehind.asmx for the C# version. Each file will consist of a single line of code, as shown in Example 16-25 and Example 16-26.

Example 16-25. vbStockTickerCodeBehind.asmx

<%@ WebService Language="vb" Class="ProgAspNet.vbStockTicker" %>

Example 16-26. csStockTickerCodeBehind.asmx

<%@ WebService Language="c#" Class="ProgAspNet.csStockTicker" %>

This WebService directive uses the same attributes for code-behind as a normal web page, as described in Chapter 6. The Language attribute specifies the language, either VB, C#, or JS for VB.NET, C#, or JScript, respectively.

The Class attribute specifies the name of the code-behind class that implements the web service. In the code in Example 16-25 and Example 16-26, the class specified is ProgAspNet.vbStockTicker or ProgAspNet.csStockTicker, depending on the language used. These are the fully qualified web service class names used in the examples in this chapter. The class names themselves have prepended to them the namespace ProgAspNet, from Example 16-1 and Example 16-2.

Herein lies a significant difference between using code-behind and using inline code. With code-behind, the code-behind class must be compiled prior to calling the .asmx file (or .aspx file for normal web pages; it works the same for both web pages and web services). The compiled dll then must be placed in a in subdirectory directly beneath the directory containing the .asmx (or .aspx) file. This is shown schematically in Figure 16-6, using the names for the C# implementation.

Using code-behind

Figure 16-6. Using code-behind

In order to create the code-behind file, follow these steps:

  1. Save the inline .asmx file being developed throughout this chapter as either StockTickerCodeBehind.vb or StockTickerCodeBehind.cs, depending on the language.

  2. Open this new code-behind file in an editor and delete the first line in the file, the WebService directive.

  3. Save the new code-behind file.

The code-behind file can then be compiled into a dll. This is done using a language-specific command from the command prompt.

Note

In order for this (or any other .NET) command line to work, the path must be set to include the executable being called. To do this manually would not be trivial. Instead, there is an item in the Start menu:

ProgramsMicrosoft Visual Studio .NETVisual Studio .NET Tools
Visual Studio .NET Command Prompt

that opens a command prompt window (what used to be known as a DOS prompt, for you old-timers) with the proper path set.

First change the current directory of the command window to be the directory containing the code-behind file. The command to do this is something like:

cd projectsprogramming asp.net

The generic syntax for the compiler is:

                  compilerExe [parameters] inputFile.ext

where compilerExe is either vbc for VB.NET or csc for C#. This is followed by one or more parameters, which is then followed by the name of the source code file being compiled.

For VB.NET, use the following single-line command to compile the DLL:

vbc /out:binvbStockTickerCodeBehind.dll /t:library /r:system.dll,system.web.
dll,system.web.services.dll,
system.data.dll,system.XML.dll StockTickerCodebehind.vb

and for C# use this single-line command:

csc /out:bincsStockTickerCodeBehind.dll /t:library /r:system.dll,system.web.
dll,system.web.services.dll StockTickerCodebehind.cs

The command-line compilers have a large number of parameters available to them, three of which are used here. In order to see the complete list of parameters available, enter the following command at the command prompt:

                  compilerExe /?

Table 16-2 lists the parameters used in the preceding command lines.

Table 16-2. Parameters used in commands to compile the dll

Parameter

Short form

Description

/out:<filename>

Output filename. If not specified, then the output filename is derived from the first source file.

/target:library

/t:library

Build a library file. Alternative values for target are exe, winexe, and module.

/reference:<file list>

/r:

Reference the specified assembly files. If more than one file, either include multiple reference parameters or separate filenames with commas within a single reference parameter. Be certain not to include any spaces between filenames.

Notice there is a correlation between the namespaces referenced in the source code and the files referenced in the compile command. Table 16-3 shows the correspondence.

Table 16-3. Correspondence of source code and compiler references

Source code reference

Compiler reference

Description

System

system.dll

Supplies fundamental classes and base classes.

-

system.web.dll

Supplies classes and interfaces to enable client/server communications. Not necessary in source code because it is referenced automatically by the ASP.NET runtime.

System.Web.Services

system.web.services.dll

Classes that enable web services.

System.Collections

-

Provides classes and interfaces used by various collections, including Arrays and ArrayLists. Not necessary in the compiler reference because it is included in mscorlib.dll, which is referenced by default.

Once the dll is created and located in the proper bin subdirectory (which the previous command lines do for you), then the .asmx file can be tested in a browser or called by a client, just like any other .asmx file.

Using Visual Studio .NET

Visual Studio .NET offers the programmer several advantages over a plain text editor, in addition to automating the creation of code-behind. Among them are color coding of the source code, integrated debugging, IntelliSense, integrated compiling, and full integration with the development environment. Use of the Visual Studio .NET IDE is covered in detail in Chapter 6.

Open Visual Studio .NET to create a web service using code-behind. Click on the New Project button to start a new project. You will be presented with the dialog box shown in Figure 16-7.

New Project dialog box

Figure 16-7. New Project dialog box

You can select a Project Type of the language of your choice. This example will use VB.NET.

Select the ASP.NET web service template.

The default name for the project will be WebService1. Change that to StockTickerVB, as shown in Figure 16-7. When you click the OK button, Visual Studio .NET will cook for few a moments, and then it will open in design mode.

Tip

Be careful if using any non-alpha characters in the project name. I originally named this project StockTicker-VB. Visual Studio .NET seemed to accept this, but on moving through the process, the hyphen was converted to an underscore under some circumstances and the project would not compile and run properly.

Pay particular attention to the Solution Explorer, located in the upper right quadrant of the screen by default, and shown in Figure 16-8.

Default Solution Explorer

Figure 16-8. Default Solution Explorer

The Solution Explorer shows most, but not all, of the files that comprise the project. In a desktop .NET application (i.e., not an ASP.NET project), all of the files in a project would be located in a subdirectory, named the same as the project name, located under the default project directory. One of these files has an extension of .sln . The .sln file tells Visual Studio .NET what files to include in the project. An associated file has an extension of .suo.

Tip

The default project location can be set by selecting Tools Options... Environment Projects and Solutions and changing the directory in the edit box.

When creating an ASP.NET project, either a web page or a web service, the project directory is still created under the default projects directory. That directory still contains the .sln and .suo files. If these files are missing, then Visual Studio .NET cannot open the project. However, all the other files comprising the project are contained in the virtual root directory of the application, which is a subdirectory with the project name created under the physical directory corresponding to localhost. On most machines the physical directory corresponding to localhost is c:inetpubwwwroot.

There are other files Visual Studio .NET does not display by default. You can force the Solution Explorer to show all files by clicking on the Show All Files icon in the Solution Explorer tool bar. (It is the second icon from the right, just below the word “StockTicker” in Figure 16-8.) Clicking on this icon and expanding all the nodes results in the Solution Explorer shown in Figure 16-9.

Expanded Solution Explorer

Figure 16-9. Expanded Solution Explorer

Under References, you can see all the namespaces that are referenced by default. Under bin, you can see any files contained in that subdirectory. (The IDE automatically put the dll and pdb files there on startup.)

The rest of the entries in the Solution Explorer are files contained in the virtual root directory of the application. For this application, that virtual root directory will typically be:

c:InetPubwwwrootStockTickerVB

AssemblyInfo.vb contains versioning information and will be covered in more detail in Chapter 20.

The global.asax file contains global configuration information for the entire application. It was covered in Chapter 6, and will be covered further in Chapter 20. Notice that it is now apparent that the global.asax file uses code-behind, with the actual Global class contained in the code-behind file (global.asax.vb for VB.NET projects, global.asax.cs for C# projects).

The .resx files are resource files created by the IDE which contain localization information.

The Service1 files contain the actual web service code. They will be covered shortly.

The StockTickerVB.vsdisco is a discovery file, used by the consumer of the web service. Discovery will be covered in Chapter 17.

Web.config is another configuration file, which was covered in Chapter 6 and will be covered further in Chapter 20.

This leaves the Service1 files. Click on either the Service1.asmx or the Service1.asmx.vb files in the Solution Explorer. Nothing appears in the design window. This is because Visual Studio .NET displays design objects by default, and web services do not use any. They are all code.

In order to see the contents of Service1.asmx, right-click on the file in the Solution Explorer, select Open With..., and select Source Code (Text) Editor from the list of choices. Looking at Service1.asmx, you will see that the file has a single line of code, shown in Example 16-27.

Example 16-27. Service1.asmx in Visual Studio .NET

<%@ WebService Language="vb" Codebehind="Service1.asmx.vb" Class="StockTickerVB.Service1" %>

Compare this to the WebService directives in Example 16-1 and Example 16-2. The Codebehind attribute is used by Visual Studio .NET to know where to find the code-behind file. The Class attribute points to the default code-behind class defined in the code-behind file.

You can view any of the other files in the Solution Explorer by right-clicking on the file and selecting either Open or View Code, as appropriate. Once a code window is open in the design window, you can switch back and forth among the various files by clicking on the correct tab at the top of the design window.

Click on the Service1.asmx file, right-click, and select View Code from the context-sensitive menu. The contents of the code-behind file, Service1.asmx.vb, are displayed. Notice that there is already code in place, as shown in Figure 16-10.

Boilerplate code in code-behind file

Figure 16-10. Boilerplate code in code-behind file

In addition to all the other infrastructure Visual Studio puts into the project, it includes the minimum necessary code to implement a code-behind web service file. The Imports statement necessary to allow the web service class, Service1, to derive from the WebService base class is added, and that class is defined, at least in skeleton form.

The class definition is followed by a collapsed region, indicated by the plus symbol along the left margin, which contains boilerplate code inserted by and necessary to the IDE.

Next comes some commented code demonstrating a very simple web method. You can delete the commented sample code.

Even though you have not yet added any custom code to this project, you can prove to yourself that this is indeed a valid web service by clicking on the Start icon, or pressing F5, to compile and run the web service. After the code-behind class is compiled and automatically placed in the bin directory, a browser will open with the familiar web service test page. However, since there are not yet any web methods defined, there will be nothing to test.

Now cut and paste the code from the code-behind file created earlier in this chapter into the code-behind file in Visual Studio .NET, Service1.asmx.vb. Be careful not to duplicate the line importing System.Web.Services. Also, be sure to put the Option lines at the very beginning of the file and the namespace opening line before the Service1 class definition with the end namespace line at the very end of the file.

The beginning of the code-behind file now looks like Figure 16-11.

Beginning of code-behind file

Figure 16-11. Beginning of code-behind file

Notice that the WebService attribute’s Description property has been slightly edited to clearly identify this as coming from Visual Studio .NET, although the Name property remains unchanged as “StockTicker.”

Before this will run correctly, the web service file, Service1.asmx, must be edited slightly to take into account the addition of the namespace to the code-behind file. Switch to that file and edit the Class attribute value by including the ProgAspNet namespace as part of the class name. The contents of the modified Service1.asmx should now look like Example 16-28.

Example 16-28. Modified Service1.asmx in Visual Studio .NET

<%@ WebService Language="vb" Codebehind="Service1.asmx.vb" Class="StockTickerVB.
ProgAspNet.Service1" %>

Now run test the web service by clicking on the Run icon or pressing F5. You should get a browser window looking something like Figure 16-12.

Visual Studio .NET code-behind test

Figure 16-12. Visual Studio .NET code-behind test

Notice that the name of the web service reflects the name property specified in the WebService attribute’s Name property. Also, the new WebService attribute’s Description property is displayed in the test. Everything else is exactly the same as the web service createdin a text editor.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.120.187