16.1 A Simple StockTickerThe StockTicker web service will expose two web methods:
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."; } } } 16.1.1 The WebService DirectiveThe 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" %> Here it is 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. 16.1.1.1 The Language attributeThe WebService directive's Language attribute specifies the language used in the web service. Legal values include C#, VB, JS, and VJ# for C#, VB.NET, JScript.NET, and J#, respectively. The value is not case-sensitive. The Language attribute is not required. If it is missing, the default value is C#. 16.1.1.2 The Class attributeThe 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 \bin 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. To specify the implementing class contained in this namespace fully, the namespace is prepended to the class name in the WebService directive.
16.1.2 Deriving from the WebService ClassIn 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:
16.1.3 Application State via HttpContextWeb 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 WebServiceOption 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 WebServiceusing 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 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. 16.1.4 The WebMethod AttributeAs 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:
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 In C#, it looks like this: [WebMethod] public string GetName(string StockSymbol) 16.1.4.1 WebMethod propertiesThe 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 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")> In C#, it looks like this: [WebMethod(BufferResponse=false, Description="Sample description")] The following sections describe the valid WebMethod properties. 16.1.4.1.1 The BufferResponse propertyBy 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)> For C#, it is this: [WebMethod(BufferResponse=false)] 16.1.4.1.2 The CacheDuration propertyWeb 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)> For C#, it is set as follows: [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. 16.1.4.1.3 The Description propertyThe 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.")> For C#, it is as follows: [WebMethod(Description="Returns the stock price for the input stock symbol.")] 16.1.4.1.4 The EnableSession propertyThe 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 in ASP.NET web services, so if the transport mechanism is something other than HTTP (say, SMTP), then the session state functionality will not be available. 16.1.4.1.5 The MessageName propertyIt 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. 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. Figure 16-1. Conflicting WebMethod namesTo 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. Figure 16-2. MessageName WSDLYou 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. 16.1.4.1.6 The TransactionOption propertyASP.NET web methods can use 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. 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, because 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:
The two values that do start a new transaction are:
In order to use transactions in a web service, you must take several additional steps:
If there are no exceptions thrown by the web method, then the transaction will automatically commit unless the SetAbort method is explicitly called. If an unhandled exception is thrown, the transaction will automatically abort. 16.1.5 The WebService AttributeThe 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( ) For C#, it is: [WebService(PropertyName=value)] public class csStockTicker( ) PropertyName is a valid property accepted by the WebService attribute (these are described later), 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")> _ In C#, it would be: [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. 16.1.5.1 The Description propertyThe 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. 16.1.5.2 The Name propertyThe 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 the section, "The WebService Attribute," 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. 16.1.5.3 The Namespace propertyEach 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 ensure it can be uniquely identified by an application. 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. 16.1.6 Data TypesASP.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.
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 code shown here is for VB .NET: <WebMethod> _ Public Sub GetArray( ) As String( ) { Dim TestArray( ) As String = {"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. (As alternative, you could convert the ArrayList to a strongly-typed array using ArrayList.ToArray( ), before retuning it.) To test this, enter the following URL into a browser. For VB.NET, use: http://localhost/ProgAspNet/vbStockTicker.asmx 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. Figure 16-4. GetList test pageFigure 16-5. GetList test resultsWeb services can also use user-defined classes and structs as either parameters or return types. The rules to remember are:
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.NETpublic class Stock public StockSymbol as string public StockName as string public Price as double public History(1) 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.
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 theStock 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 theStock.StockSymbol = StockSymbol theStock.StockName = stocks(i,1) theStock.Price = Convert.ToDouble(stocks(i,2)) ' Populate the StockHistory data. theStock.History(0) = new StockHistory( ) theStock.History(0).TradeDate = Convert.ToDateTime("5/1/2001") theStock.History(0).Price = Convert.ToDouble(23.25) theStock.History(1) = new StockHistory( ) theStock.History(1).TradeDate = Convert.ToDateTime("6/1/2001") theStock.History(1).Price = Convert.ToDouble(28.75) return theStock end if next theStock.StockSymbol = StockSymbol theStock.StockName = "Stock not found." return theStock 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.
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.NETImports 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 theStock 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 theStock.StockSymbol = StockSymbol theStock.StockName = stocks(i,1) theStock.Price = Convert.ToDouble(stocks(i,2)) ' Populate the StockHistory data. theStock.History(0) = new StockHistory( ) theStock.History(0).TradeDate = _ Convert.ToDateTime("5/1/2001") theStock.History(0).Price = Convert.ToDouble(23.25) theStock.History(1) = new StockHistory( ) theStock.History(1).TradeDate = _ Convert.ToDateTime("6/1/2001") theStock.History(1).Price = Convert.ToDouble(28.75) return theStock end if next theStock.StockSymbol = StockSymbol theStock.StockName = "Stock not found." return theStock 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; } } } 16.1.7 Using Code-BehindWhen you are creating web pages, code-behind allows you to separate application logic from design or user interface (UI) elements.
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. 16.1.7.1 Using a text editorIn 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#, JS, or VJ# for VB.NET, C#, JScript, or J#, 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. When using just the Class attribute, 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 \bin 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. Figure 16-6. Using code-behindTo create the code-behind file, follow these steps:
The code-behind file can then be compiled into a dll. This is done using a language-specific command from the command prompt.
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 this: cd projects\programming 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:bin\vbStockTickerCodeBehind.dll /t:library /r:system.dll,system.web. dll,system.web.services.dll, system.data.dll,system.XML.dll StockTickerCodebehind.vb For C#, use this single-line command: csc /out:bin\csStockTickerCodeBehind.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. 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.
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.
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, as long as the \bin directory is a subdirectory of the proper virtual directory. 16.1.7.2 Using Visual Studio .NETVisual 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. Chapter 6 discusses in detail the use of the Visual Studio .NET IDE. 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. Figure 16-7. New Project dialog boxYou 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.
Pay particular attention to the Solution Explorer, located in the upper-right quadrant of the screen by default, and shown in Figure 16-8. Figure 16-8. Default Solution ExplorerThe 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.
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:\inetpub\wwwroot. 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. Figure 16-9. Expanded Solution ExplorerUnder 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:\InetPub\wwwroot\StockTickerVB 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. 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. Figure 16-10. Boilerplate code in code-behind fileIn 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. Figure 16-11. Beginning of code-behind fileNotice 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. Figure 16-12. Visual Studio .NET code-behind testNotice 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 created in a text editor. |