Web Service Description and Data Types

I l @ ve RuBoard

You've seen that creating a Web service involves writing a simple J# class and that the ASP.NET runtime does the rest. The ASP.NET runtime does a lot of work on your behalf , but sometimes you need more control over how your Web service appears to clients and the style of service it provides. One way you can do this is by adapting the description of your service. This effort might involve changing names and URIs, mapping types, and configuring the handling for custom types. Let's look at what you can do to customize service description and data type handling.

Exposing a Web Service Interface

When you tested out the simple Web service, you saw the screen shown earlier in Figure 17-2. Clicking the Service Description hyperlink on the right side of the screen displays the WSDL description of the service. This WSDL document is generated by the HTTP pipeline (part of ASP.NET), which recognizes the ASMX file extension and uses reflection to examine the class in search of Web service attributes. You've already seen how you can use the WebMethod attribute to annotate methods and have them exposed as part of your Web service.

This WSDL description is important because it provides the only source of information from which a client can determine the functionality provided by a Web service and how to access it. To control the contents of this document, you can apply various additional Web service attributes to classes, methods, and variables . All of these attributes are identified as part of the reflection process, and the generated WSDL is altered appropriately. We'll examine and tweak the WSDL document generated from the Web service class to suit our purposes.

Namespaces in WSDL

The root element of the WSDL document, definitions , contains various namespace definitions:

 <definitionsxmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:s0="http://tempuri.org/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" targetNamespace="http://tempuri.org/" xmlns="http://schemas.xmlsoap.org/wsdl/"> 

Because WSDL is an XML grammar, it has a schema that defines the structure of a WSDL document. This schema is defined at http://www.schemas.xmlsoap.org/wsdl/, and this URL is used as the default namespace for the document. Namespace prefixes are also defined for various standard encodings and structural information that might be required in the WSDL document, namely:

  • A schema that defines how to describe SOAP message structures in WSDL documents. (An association between this schema and the namespace prefix soap is specified by the definition starting with xmlns:soap .)

  • The encoding of complex types in SOAP messages ( xmlns:soapenc )

  • A schema that defines how to describe HTTP message structures in a WSDL document ( xmlns:http )

  • The MIME message structure/encoding for WSDL documents ( xmlns:mime )

  • A reference to the XML Schema type definitions ( xmlns:s ) that are used to define parameter types and complex types.

Finally, two namespaces relate to the Web service itself:

  • xmlns:s0 , which is used as a marker for complex type mappings within the document

  • targetNamespace , which specifies that all of the names declared in this definition belong to the given namespace

By default, these last two namespaces are set to http://tempuri.org/ , which is a dummy URL registered specifically for use as a temporary marker in generated XML Web service documents. It is usually acceptable to use http://tempuri.org/ while you're developing your Web service in the safety of your own project group , but you must change this URI before you publish your Web service more widely. If you read the warning contained in the description screen for SimpleEnquiry.asmx (shown in Figure 17-2), you already know that it is important to assign a unique identity to your Web service so clients can differentiate your Web service from those of other Web service creators. The identity used for a Web service is typically based on the Web address of the organization or person creating the service. Because domain names are unique across the Web, they guarantee the uniqueness of identities based on them ( assuming that your company has a policy to ensure that two service creators within the company do not use the same path in addition to the base URL).

You can use the WebService attribute to define the namespace associated with a particular Web service class. You can see this in the sample file Enquiry.asmx (part of the CakeCatalogService project), which contains the same FeedsHowMany method as the SimpleEnquiry.asmx sample file but was created using Visual Studio .NET. The following code changes the namespace associated with the Enquiry class from http://tempuri.org/ to http://fourthcoffee.com/CakeCatalog/ :

 /**@attributeWebServiceAttribute(Namespace= "http://fourthcoffee.com/CakeCatalog/")*/ publicclassEnquiryextendsSystem.Web.Services.WebService { } 

This action changes the generated WSDL document as shown here:

 <definitionsxmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:s0="http://fourthcoffee.com/CakeCatalog/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" targetNamespace="http://fourthcoffee.com/CakeCatalog/" xmlns="http://schemas.xmlsoap.org/wsdl/"> 

If you access the Enquiry service through a Web browser, you'll also see that the namespace warning is not displayed.

Type Definitions

WSDL documents build up to the service definition in a bottom-up manner. The first definitions in the document are for any complex data types used in the Web service messages passed between the client and the service.

The XML Schema standard defines a set of basic types, such as strings and integers. You can use these basic types to define more complex types by combining them, in much the same way that you would create an object or data structure by combining a group of basic types. WSDL uses complex types to define the sets of parameters and return values that are passed as part of a method call. Recall the signature for FeedsHowMany :

 publicintFeedsHowMany(intdiameter,Stringshape, Stringfilling) 

Given this signature, the WSDL generator in ASP.NET will create two WSDL complex types, as defined by the schema element within the types element:

 <types> <s:schemaelementFormDefault="qualified" targetNamespace= "http://fourthcoffee.com/CakeCatalog"> <s:elementname="FeedsHowMany"> <s:complexType> <s:sequence> <s:elementminOccurs="1" maxOccurs="1" name="diameter" type="s:int" /> <s:elementminOccurs="0" maxOccurs="1" name="shape"  type="s:string" /> <s:elementminOccurs="0" maxOccurs="1" name="filling" type="s:string" /> </s:sequence> </s:complexType> </s:element> <s:elementname="FeedsHowManyResponse"> <s:complexType> <s:sequence> <s:elementminOccurs="1" maxOccurs="1" name="FeedsHowManyResult" type="s:int" /> </s:sequence> </s:complexType> </s:element> <s:elementname="int" type="s:int" /> </s:schema> </types> 

The complex type named FeedsHowMany represents the parameters passed into the method call ”the diameter (an integer), shape (a string), and filling (another string). The complex type named FeedsHowManyResponse represents the return values from the method call, in this case a single integer. Notice that the target namespace for these type definitions is the same as the target namespace for the overall document. These complex types are used as part of the SOAP message definitions later in the WSDL document.

Later in the chapter, you'll see how more complex data types, such as object types, are mapped into WSDL.

Messages, Parameters, and Parts

The set of data types defined for describing parameters and return values are used in WSDL to build up descriptions of the messages passed between client and server. In SOAP terms, this means defining two messages, a request and a response:

 <messagename="FeedsHowManySoapIn"> <partname="parameters" element="s0:FeedsHowMany" /> </message> <messagename="FeedsHowManySoapOut"> <partname="parameters" element="s0:FeedsHowManyResponse" /> </message> 

The FeedsHowManySoapIn message has a single SOAP part that carries a FeedsHowMany type, which contains the parameter values sent by the client. The FeedsHowManySoapOut message also contains a single SOAP part. In this case, it carries a FeedsHowManyResponse type containing the return value from the method.

As noted earlier, two HTTP bindings are provided by default in addition to the SOAP binding. These bindings do not use the complex type definitions from the types element but specify them as part of the message definition, as shown in the following POST message definitions:

 <messagename="FeedsHowManyHttpPostIn"> <partname="diameter" type="s:string" /> <partname="shape" type="s:string" /> <partname="filling" type="s:string" /> </message> <messagename="FeedsHowManyHttpPostOut"> <partname="Body" element="s0:int" /> </message> 

The names of the SOAP and HTTP messages are based on the Web method name. If the exposed name needs to be different (for example, if you want to disambiguate overloaded methods, as discussed later), you can alter it using the WebMethod attribute, as shown in the following code fragment taken from the EnquiryWithRenaming.asmx.jsl sample file:

 /**@attributeWebMethodAttribute(MessageName="CakeCapacity", Description="Tellsyouhowmanypeopleagivencakefeeds") */ publicintFeedsHowMany(intdiameter,System.Stringshape, System.Stringfilling) 

Tip

You can spread attribute declarations across multiple lines as shown in preceding code, but do not insert extra asterisks (*) at the start of continuation lines ”this will cause compilation problems.


Defining the MessageName property for the WebMethod attribute changes the names of the SOAP and HTTP messages and of the complex types in the WSDL that is generated, as shown here:

 <types> <s:schemaelementFormDefault="qualified" targetNamespace="http://fourthcoffee.com/CakeCatalog/"> <  s:elementname="CakeCapacity  "> <s:complexType> <s:sequence> <s:elementminOccurs="1" maxOccurs="1" name="diameter" type="s:int" /> <s:elementminOccurs="0" maxOccurs="1" name="shape" type="s:string" /> <s:elementminOccurs="0" maxOccurs="1" name="filling" type="s:string" /> </s:sequence> </s:complexType> </s:element> <s:elementname="CakeCapacityResponse"> <s:complexType> <s:sequence> <s:elementminOccurs="1" maxOccurs="1" name="CakeCapacityResult" type="s:int" /> </s:sequence> </s:complexType> </s:element> <s:elementname="int" type="s:int" /> </s:schema> </types>  <messagename="CakeCapacitySoapIn"> <partname="parameters" element="s0:CakeCapacity" />  </message>  <messagename="CakeCapacitySoapOut"> <partname="parameters" element="s0:CakeCapacityResponse" />  </message> 

You might have noticed that a description is also defined for the Feeds ­HowMany method in EnquiryWithRenaming.asmx.jsl. This description does not form any part of the WSDL associated with the message, but it shows up in the PortType (as discussed later). It also forms part of the service description HTML page that is displayed when you access the basic URL for this Web service, as shown in Figure 17-6.

Figure 17-6. The Web service HTML description can contain textual method descriptions.

WSDL syntax does not allow for overloaded methods. This will not stop you from implementing overloaded methods on a class, but only one of them can be tagged with the WebMethod attribute. If you try to apply the WebMethod attribute to more than one overloaded method, as shown in the sample file EnquiryWithOverloading.asmx.jsl, your class will compile but you'll get the following runtime exception:

Tip

System.InvalidOperationException : Both Int32 FeedsHowMany ( Int32 , System.String , System.String ) and Void FeedsHowMany ( Int32 , System.String ) use the message name 'FeedsHowMany' . Use the MessageName property of the WebMethod custom attribute to specify unique message names for the methods.

If you really need to expose overloaded methods, you can use the MessageName of the WebMethod attribute to change the names of the overloaded methods.


Naming Parameters and Return Values

For SOAP messages, you use an XmlSerializer to convert the J# data types into XML. This means that in addition to the Web service-specific attributes you've seen so far, you can also use the XML serialization attributes contained in the System.Xml.Serialization namespace to alter the results of the conversion. You can use these XML- related attributes to change the name of a parameter or return value. (You'll see other uses later when we discuss type conversion.)

Caution

Be careful when you change the names of parameters. SOAP messages are text-based, so the mapping of parameters in the message to parameters of the method is performed based on the name of the parameter. If you change the name of the parameter on the server but do not make a matching change on the client, the call won't fail, but the "missing" parameter will be assigned a default value when the Web service runtime invokes the method call.


To change the name of the shape parameter to FeedsHowMany , use the XmlElementAttribute , as shown in the EnquiryWithRenaming.asmx.jsl sample file:

 public intFeedsHowMany(intdiameter, /**@attributeXmlElementAttribute("style")*/Stringshape, Stringfilling) 

This code causes the shape parameter to appear with the name style in the WSDL complex type description:

 <s:elementname="CakeCapacity"> <s:complexType> <s:sequence> <s:elementminOccurs="1" maxOccurs="1" name="diameter" type="s:int" /> <  s:elementminOccurs="0" maxOccurs="1" name="style" type="s:string" /  > <s:elementminOccurs="0" maxOccurs="1" name="filling" type="s:string" /> </s:sequence> </s:complexType> </s:element> 

Note

The XmlElement attribute will not change the name exposed in the HTTP POST and GET message definitions because the XML formatter is not used to process the message contents in these cases.


You can also change the name associated with the return value from the method using the @attribute.return directive (again, taken from the sample file EnquiryWithRenaming.asmx.jsl), as shown here:

 /**@attribute.returnXmlElementAttribute(ElementName="NumberOfPeople") */ /**@attributeWebMethodAttribute(MessageName="CakeCapacity", Description="Tellsyouhowmanypeopleagivencakefeeds") */ public intFeedsHowMany(intdiameter, /**@attributeXmlElementAttribute("style")*/Stringshape, Stringfilling) 

This changes the definition of the response complex type:

 <s:elementname="CakeCapacityResponse"> <s:complexType> <s:sequence> <s:elementminOccurs="1" maxOccurs="1" name="NumberOfPeople" type="s:int" /> </s:sequence> </s:complexType> </s:element> 

You can also change the type mapping of the return value, as you'll see later.

Operations, Port Types, and Bindings

Once you've defined the set of messages that a Web service will use, you can group them into portType elements. You can think of a portType element as equivalent to an interface definition in the Java language. It provides a way to assign a set of messages into a cohesive, logical group. The following portType indicates that the CakeCapacitySoapIn and CakeCapacitySoapOut messages are the input and output of one logical operation. This operation represents a SOAP request and response.

 <portTypename="EnquiryWithRenamingSoap"> <operationname="FeedsHowMany"> <documentation>Tellsyouhowmanypeopleagivencake feeds</documentation> <inputname="CakeCapacity" message="s0:CakeCapacitySoapIn" /> <outputname="CakeCapacity" message="s0:CakeCapacitySoapOut" /> </operation> </portType> 

Note that the operation name is FeedsHowMany rather than CakeCapacity . To change the operation name, you must use the SOAP-specific attributes discussed later. Also notice that the method description defined earlier using the WebMethod attribute appears here.

This is the point at which traditional IDLs such as COM IDL and CORBA IDL stop. However, because this is an XML Web service, you also need to know where to find the particular instance of the service and which protocols you can use to communicate with it. WSDL lets you associate a protocol with a portType to create a binding element:

 <bindingname=" EnquiryWithRenamingSoap" type="s0:EnquiryWithRenamingSoap"> <soap:bindingtransport="http://schemas.xmlsoap.org/soap/http" style="document" /> <operationname="FeedsHowMany"> <soap:operation soapAction="http://fourthcoffee.com/CakeCatalog/CakeCapacity" style="document" /> <inputname="CakeCapacity"> <soap:bodyuse="literal" /> </input> <outputname="CakeCapacity"> <soap:bodyuse="literal" /> </output> </operation> </binding> 

The type attribute of the binding element indicates that the binding is associated with the EnquiryWithRenamingSoap portType . The contents of the binding element are similar to those of the portType , but they provide SOAP-specific information:

  • The transport attribute in the soap:binding element indicates that this binding defines how to pass the given messages over HTTP. The URI http://schemas.xmlsoap.org/soap/http is used for HTTP bindings; other URIs can be used to bind messages to alternative transports such as FTP or SMTP.

  • The soapAction attribute of the soap:operation element is a value that should be assigned to the HTTP SOAPAction header when the client is creating this SOAP call. This value can be used by the server to route the call to the correct method. However, this information is used only for HTTP bindings and is somewhat redundant because the name of the method also occurs as the name of the outermost element in the SOAP body.

  • The remaining attributes ( use on soap:body , and style on soap:operation and soap:binding ) all relate to the way in which the parameters and return values are encoded. This is discussed in the following sidebar.

Document Literal vs. SOAP Encoding and RPC

You can look at a SOAP message in two ways. One approach is to continue the RPC-style analogy and say that each parameter and return value should be treated as an individual item to be defined in your call. Each item has its own separate part of the SOAP message in which it is defined. This is known as an "RPC-style" SOAP message, as defined in Section 7 of the SOAP specification. In conjunction with this RPC-style way of structuring a message, you also need a way of encoding particular data types in XML that you can use when you convert the parameter and return values into and out of XML. In the absence of any formal standard, you have to invent your own. This was the main approach taken by the original SOAP specification. When the specification was written, there was no standard XML Schema definition to provide a core set of XML-based data types, so the specification writers defined their own data encoding rules in Section 5 of the SOAP specification. (Hence this style of encoding is referred to as "SOAP encoding.")

When the XML Schema standard and its associated data types (the Information Set, or InfoSet for short) were defined, it became clear that they offered a much more flexible way of encoding information passed in SOAP messages. You can define a schema that describes the data to be passed in your SOAP call. One complex type can be defined to describe the set of parameters passed in and another can describe the return value(s). Your SOAP call will then become the exchange of two XML documents. This is known as "document-style" SOAP messaging. The contents of these XML documents are defined by a schema, so they can be validated , and their potential contents are as flexible as XML itself. The use of schemas and XML documents to define the contents of SOAP messages is referred to as literal encoding .

Nothing is intrinsically wrong with the SOAP encoding and RPC approach, but the document/literal approach is considered a more powerful way of exchanging SOAP messages. Newer SOAP-related products and specifications tend to default to the document/literal approach ”including Visual Studio .NET and the .NET Framework. This might cause some short- term interoperability issues (discussed briefly later), but the adoption of document-style and literal encoding brings with it far more flexibility and extensibility.

Services and Ports

The final part of the WSDL document is the service definition itself. This consists of a set of ports that make up the service. A port specifies the endpoint at which a server implementing a particular binding can be found. In the case of our simple service, there are three port elements, one for the SOAP binding, one for the HTTP POST binding, and one for the HTTP GET binding:

 <  servicename="EnquiryWithRenaming"  > <  portname="EnquiryWithRenamingSoap" binding="s0:EnquiryWithRenamingSoap"  > <  soap:addresslocation= "http://localhost/CakeCatalogService/EnquiryWithRenaming.asmx"/  >  </port  > <portname="EnquiryWithRenamingHttpGet" binding="s0:EnquiryWithRenamingHttpGet"> <http:addresslocation=  "http://localhost/CakeCatalogService/EnquiryWithRenaming.asmx"/> </port> <portname="EnquiryWithRenamingHttpPost" binding="s0:EnquiryWithRenamingHttpPost"> <http:addresslocation= "http://localhost/CakeCatalogService/EnquiryWithRenaming.asmx"/> </port> </service> 

The binding attribute of the first port element associates it with the EnquiryWithRenamingSoap binding. The location attribute of the soap:address element indicates that you'll find a server at http://localhost/CakeCatalogService/EnquiryWithRenaming.asmx that implements this binding.

The service element has a name attribute that you can change using the WebService attribute you saw earlier. By default, the name of an XML Web service based on ASP.NET is the name of the code-behind class ”in this case, EnquiryWithRenaming . The following example changes the name of the service to CakeCatalogEnquiry :

 /**@attributeWebServiceAttribute(Namespace= "http://fourthcoffee.com/CakeCatalog/", Name="CakeCatalogEnquiry", Description="EnquiryserviceforFourthCoffee'sCakeCatalog")*/ publicclassEnquiryextendsSystem.Web.Services.WebService { } 

This action results in changes to the names used throughout the WSDL document for service , binding , and portType elements, as shown here:

 <portTypename="CakeCatalogEnquirySoap"> <operationname="FeedsHowMany"> <documentation>Tellsyouhowmanypeopleagivencake feeds</documentation> <inputname="CakeCapacity" message="s0:CakeCapacitySoapIn" /> <outputname="CakeCapacity" message="s0:CakeCapacitySoapOut" /> </operation> </portType> <bindingname="CakeCatalogEnquirySoap" type="s0:CakeCatalogEnquirySoap"> <soap:bindingtransport="http://schemas.xmlsoap.org/soap/http" style="document" /> <operationname="FeedsHowMany"> <soap:operation soapAction="http://fourthcoffee.com/CakeCatalog/CakeCapacity" style="document" /> <inputname="CakeCapacity"> <soap:bodyuse="literal" /> </input> <outputname="CakeCapacity"> <soap:bodyuse="literal" /> </output> </operation> </binding> <servicename="CakeCatalogEnquiry"> <documentation>EnquiryserviceforFourthCoffee'sCakeCatalog </documentation> <portname="CakeCatalogEnquirySoap" binding="s0:CakeCatalogEnquirySoap"> <soap:addresslocation=  "http://localhost/CakeCatalogService/EnquiryWithRenaming.asmx" /> </port> </service> 

The description string defined in the WebService attribute becomes a documentation element inside the service element. Again, this is shown as part of the HTML description generated by ASP.NET, as shown in Figure 17-7.

Figure 17-7. You can set the Web service HTML description using the WebService attribute.

Invoking the Service

Once the service has been defined, clients can access it. Given the description so far of the CakeCatalogService , a client can formulate a SOAP call as shown here and send it to the Web server on localhost :

 POST/CakeCatalogService/EnquiryWithRenaming.asmxHTTP/1.1 User-Agent:Mozilla/4.0(compatible;MSIE6.0;MSWebServicesClientProtocol1.0.3705.0) Content-Type:text/xml;charset=utf-8 SOAPAction: "http://fourthcoffee.com/CakeCatalog/CakeCapacity" Content-Length:400 Expect:100-continue Connection:Keep-Alive Host:localhost <?xmlversion="1.0" encoding="utf-8"?> <soap:Envelopexmlns:soap=http://schemas.xmlsoap.org/soap/envelope/ xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <CakeCapacityxmlns="http://fourthcoffee.com/CakeCatalog/"> <diameter>10</diameter> <style>square</style> <filling>fruit</filling> </CakeCapacity> </soap:Body> </soap:Envelope> 

As long as the service is running on localhost , this request should elicit the following response:

 HTTP/1.1200OK Server:Microsoft-IIS/5.1 Date:Tue,23Apr200211:27:52GMT Cache-Control:private,max-age=0 Content-Type:text/xml;charset=utf-8 Content-Length:371 <?xmlversion="1.0" encoding="utf-8"?> <soap:Envelopexmlns:soap=http://schemas.xmlsoap.org/soap/envelope/ xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <CakeCapacityResponsexmlns="http://fourthcoffee.com/CakeCatalog/"> <NumberOfPeople>25</NumberOfPeople> </CakeCapacityResponse> </soap:Body> </soap:Envelope> 

Note

These SOAP calls have been tidied up for readability. Please don't count the characters ”the added linefeeds will make them add up to more than 400 and 371 characters , respectively!


Obviously, you don't want to have to write the low-level socket or HTTP code. Fortunately, Visual Studio .NET provides a way to import a WSDL description for a service and generate a Web service proxy that hides these details, making a Web service appear like a local method call to a client. The creation and use of proxies is discussed in the next chapter.

Passing Complex Data Types

The types used so far by the example XML Web service have been simple types. But in many cases, providing information as a set of simple, individual parameters might be inefficient or difficult to maintain. Many designs require that complex types be passed across component boundaries. These types generally represent complex domain concepts such as a customer or an order. They provide a central point of change for these descriptions and make the system easier to maintain. (If you add a customer ID to the customer type, all uses of the customer type will instantly have this modification ”you won't have to add a new parameter to a whole bunch of methods.)

How do such domain objects map into WSDL and Web services? If you're developing a cake ordering system to be written in J#, at some point in your design you'll undoubtedly define an Order class that contains the data and probably some business logic related to the creation and manipulation of the order. You can implement this concept in various ways. You can create a full-blown class with properties and methods, or you can define a simple data-only class to hold the data and rely on other components for the business logic. (You might implement this as a struct in C#.) Alternatively, you can create a typed ADO.NET DataSet to hold many orders and provide manipulation functions based on it. Any of these options are fine in the .NET world. The best choice will depend on the context of your application and how that type fits in the application.

As you saw in Chapter 11, you can use .NET Remoting to distribute the components of such applications and pass complex types back and forth using serialization. However, if you intend to offer the functionality of your application as an XML Web service, different rules will apply. We'll talk about passing DataSet objects across Web service boundaries shortly, but for now, consider passing domain-specific objects across a Web service interface.

Representing Objects in WSDL

As an example, look at the SimpleOrder class defined in the SimpleOrder.jsl sample file that is part of the CakeCatalogService project. This class contains:

  • Four properties: a customer name, an order ID, an item description, and the number of those items requested . These are defined using private member variables and public property accessors (getters and setters).

  • A default constructor and a constructor that allows you to create a fully populated object.

  • A method containing some simple data validation that can be invoked to check that the order meets the constraints on its data.

In a typical object-oriented system, you can pass instances of this object to a hypothetical SubmitSimpleOrder method. This method will probably return another complex type called Receipt that contains:

  • Three read-only properties for the customer name, the order ID, and a timestamp

  • A single, all-at-once constructor for populating the receipt

This class is defined in the Receipt.jsl sample file. The SubmitSimpleOrder method would probably look something like this:

 /**@attributeWebMethodAttribute()*/ publicReceiptSubmitSimpleOrder(SimpleOrderorder) { //Shouldprobablyhavesomedatabasestuffhere... Receiptreceipt=newReceipt(order.get_CustomerName(), order.get_OrderId(), DateTime.get_Now()); returnreceipt; } 

So, how does this map to an XML Web service operation? You can use the reflection capabilities of ASP.NET to find out.

If you define the SubmitSimpleOrder method shown above in a Web service called SimpleOrdering , for example, and then point your browser at http://localhost/CakeCatalogService/SimpleOrdering.asmx , the first thing you would find is that an exception is thrown because the Receipt class does not have a default constructor. It therefore cannot be serialized and passed across a network boundary. This is the same restriction placed on classes that are transmitted using .NET Remoting. A version of the Receipt class with a default constructor can be found in the sample file SerializableReceipt.jsl.

Using the updated form of the receipt, you'll get the following type definitions in your WSDL description of the Ordering service. (You can see this by viewing the WSDL for the SimpleOrderingWithBadReceipt sample Web service.)

 <types> <s:schemaelementFormDefault="qualified" targetNamespace="http://fourthcoffee.com/CakeCatalog/"> <s:elementname="SubmitSimpleOrder"> <s:complexType> <s:sequence> <  s:elementminOccurs="0" maxOccurs="1" name="order" type="s0:SimpleOrder" /  > </s:sequence> </s:complexType> </s:element> <  s:complexTypename="SimpleOrder"  > <  s:sequence  > <  s:elementminOccurs="0" maxOccurs="1" name="CustomerName" type="s:string" /  > <  s:elementminOccurs="0" maxOccurs="1" name="ItemDescription" type="s:string" /  > <  s:elementminOccurs="1" maxOccurs="1" name="ItemQuantity" type="s:int" /  > <  s:elementminOccurs="1" maxOccurs="1" name="OrderId" type="s:int" /  > <  /s:sequence  > <  /s:complexType  > <s:elementname="SubmitSimpleOrderResponse"> <s:complexType> <s:sequence> <  s:elementminOccurs="0" maxOccurs="1" name="SubmitSimpleOrderResult" type="s0:SerializableReceipt" /  > </s:sequence> </s:complexType> </s:element> <  s:complexTypename="SerializableReceipt" /  > </s:schema> </types> 

The first thing to note here is that the SubmitSimpleOrder element is defined as containing a complex type, called SimpleOrder . The definition for SimpleOrder contains type information indicating that a SimpleOrder element will contain CustomerName and ItemDescription strings (which can be null , hence the attributes minOccurs="0" and maxOccurs="1") and integer values for ItemQuantity and OrderId . However, if you examine the information generated for the SerializableReceipt class, you'll see that no information is defined for it in the WSDL except to say that it is a complex type. What's going on here?

ASP.NET Web services use the XmlSerializer (described in Chapter 10) to export .NET data into XML. This means that the data to be exported must follow the rules of the XmlSerializer ” namely, it must be publicly visible outside the class. To appear in the WSDL type definitions, data must be declared in one of the following ways:

  • As an instance variable with public visibility. This is generally accessible but leaves the data open to abuse by external classes ”for example, external classes can set an object variable to null even if this is an invalid value.

  • As a writable property. If your intention is to create an immutable object, such as a receipt, you might be tempted to create only get accessors for the data. However, if you do not provide set accessors, the values will not be generated in the WSDL and so will not be visible on the client. In other words, using a read-only property will not generate anything in XML. The property must be writable.

If you examine the SimpleOrder class, you'll see that its instance data is made visible through properties:

 publicclassSimpleOrder { privateStringcustomerName; /**@property*/ publicvoidset_CustomerName(Stringname) { if(name!=null&&name.get_Length()!=0) { customerName=name; } } /**@property*/ publicStringget_CustomerName() { returncustomerName; } privateintorderId; /**@property*/ publicvoidset_OrderId(intid) { orderId=id; } /**@property*/ publicintget_OrderId() { returnorderId; } ...} 

However, the variables in the SerializableReceipt class are read-only and are accessible through get accessors:

 publicclassSerializableReceipt { privateStringcustomerName; /**@property*/ publicStringget_CustomerName() { returncustomerName; } 

The XmlSerializer will not expose the properties of the SerializableReceipt class to the client, so they do not appear in the WSDL description of the Web service. To make the fields in the SerializableReceipt class visible in the WSDL document, you should create property accessors for them, as shown in the SimpleWsReceipt.jsl sample file:

 publicclassSimpleWsReceipt { privateStringcustomerName; /**@property*/ publicStringget_CustomerName() { returncustomerName; } /**@property*/ publicvoidset_CustomerName(Stringname) { customerName=name; } 

The resulting WSDL looks like this:

 <types> <s:schemaelementFormDefault="qualified" targetNamespace="http://fourthcoffee.com/CakeCatalog/"> <s:elementname="SubmitSimpleOrderResponse"> <s:complexType> <s:sequence> <  s:elementminOccurs="0" maxOccurs="1" name="SubmitSimpleOrderResult" type="s0:SimpleWsReceipt" /  > </s:sequence> </s:complexType> </s:element> <  s:complexTypename="SimpleWsReceipt"  > <  s:sequence  > <  s:elementminOccurs="0" maxOccurs="1" name="CustomerName" type="s:string" /  > <  s:elementminOccurs="1" maxOccurs="1" name="OrderId" type="s:int" /  > <  s:elementminOccurs="1" maxOccurs="1" name="Timestamp" type="s:dateTime" /  > <  /s:sequence  > <  /s:complexType  > </s:schema> </types> 

The description of the receipt in SimpleWsReceipt includes all of the receipt data, and this data will be marshaled and unmarshaled correctly between XML and the J# class. You can see this by examining the WSDL generated by the SimpleOrdering.asmx sample file.

Consequences of XML Serialization

As you've seen, to automatically export and import complex types between .NET and XML, you might have to greatly simplify your classes, effectively down to the level of a data structure. All of the data must be publicly visible and writable.

Although this might be initially disappointing, if you step back and think about it, it does make sense. When you call a method in a Web service or implement a Web service called by others, part of your system is outside of the .NET platform. The client for your Web service might be written in J#, C#, or Visual Basic .NET ”or in the Java language, Perl, C++, or even Cobol. The type information described in the WSDL document must make sense in all of those languages and environments. You cannot export your .NET class in its native form to these environments. All you can really send is the data it contains. If this data needs functionality to manipulate or protect it at the receiving end, this functionality must be implemented on that platform and in that language.

At the simplest level, you can take the simple data-oriented class exposed through the Web service and manipulate the data members directly on the client. If it is important to you to reassociate the functionality with the data, you can reimplement your class on the receiving platform (in a suitable language). You can then perform custom deserialization of this part of the SOAP message so that it populates one of these native classes on the receiving platform. Similarly, you would have to perform the equivalent serialization when you pass one of these classes as part of your method call on the Web service. Such custom serialization is quite possible using popular Web service environments such as the .NET Framework and the Apache Axis toolkit, but these topics are beyond the scope of this chapter. For more information, look up the <soapInterop> element in the "Remoting Settings Schema" section of the .NET Framework documentation.

One main point to take from this discussion is that even if you're using the .NET Framework at both ends of your Web service method call, doing so will not automatically serialize and reconstitute native .NET objects at both ends. An added complication is that you must make sure that the appropriate type information is available at both ends. For example, if you're serializing an object of type com.FourthCoffee.CakeCatalog.Order into XML as part of the Web service and you want to reconstitute it on the client, the assembly containing com.FourthCoffee.CakeCatalog.Order must be available to the client (either locally, in the GAC, or downloadable from a URL). The bottom line is that if you want complex types, such as objects, to appear the same at both ends, you need to do some work to make it happen.

Nested Complex Types

If you need to model nested objects, such as an order and a list of items in that order, nested serialization will take place automatically, as long as the nested class also has its data available as publicly accessible member variables. You can modify the SimpleOrder class to create a new class called Order that contains an array of line items (as shown in the sample files Order.jsl and Item.jsl). You can use this Order class to submit an order containing several line items. The sample file Ordering.asmx is an evolved form of the SimpleOrdering.asmx file that uses this Order class. The Order class is represented in WSDL as follows :

 <s:elementname="SubmitOrder"> <s:complexType> <s:sequence> <s:elementminOccurs="0" maxOccurs="1" name="order" type="s0:Order" /> </s:sequence> </s:complexType> </s:element> <s:complexTypename="Order"> <s:sequence> <s:elementminOccurs="0" maxOccurs="1" name="customerName" type="s:string" /> <s:elementminOccurs="1" maxOccurs="1" name="orderId" type="s:int" /> <s:elementminOccurs="0" maxOccurs="1" name="items" type="s0:ArrayOfItem" /> </s:sequence> </s:complexType> <  s:complexTypename="ArrayOfItem"  > <  s:sequence  > <  s:elementminOccurs="0" maxOccurs="unbounded" name="Item" nillable="true" type="s0:Item" /  > <  /s:sequence  > <  /s:complexType  > <s:complexTypename="Item"> <s:sequence> <s:elementminOccurs="0" maxOccurs="1" name="itemDescription" type="s:string" /> <s:elementminOccurs="1" maxOccurs="1" name="quantity" type="s:int" /> </s:sequence> </s:complexType> 

The WSDL defines the Order complex type, which includes an element called items (the name of the Item[] variable in the Order class), which has the type ArrayOfItem . The ArrayOfItem complex type definition indicates that it is a sequence of zero or more Item complex types, each of which contains a string description and an integer quantity . A client using an XmlSerializer will generate the following SOAP request using this WSDL description:

 POST/CakeCatalogService/Ordering.asmxHTTP/1.1 User-Agent:Mozilla/4.0(compatible;MSIE6.0;MSWebServicesClientProtocol1.0.3705.0) Content-Type:text/xml;charset=utf-8 SOAPAction: "http://fourthcoffee.com/CakeCatalog/SubmitOrder" Content-Length:576 Expect:100-continue Connection:Keep-Alive Host:localhost <?xmlversion="1.0" encoding="utf-8"?> <soap:Envelopexmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <SubmitOrderxmlns="http://fourthcoffee.com/CakeCatalog/">  <order> <customerName>PeterWaxman</customerName> <orderId>12345</orderId> <items> <Item> <itemDescription>BirthdayCake</itemDescription> <quantity>7</quantity> </Item> <Item> <itemDescription>PartyCake</itemDescription> <quantity>3</quantity> </Item> </items> </order>  </SubmitOrder> </soap:Body> </soap:Envelope> 

As you can see, the XmlSerializer will convert the array of Item classes into the XML element items . The items element is then nested inside the order element.

At first sight, you might be tempted to replace the array of Items with some form of collection available in the System.Collections namespace in the .NET Framework Class Library. This would certainly reduce the amount of effort involved in processing the array, particularly if you're dynamically adding and removing items. However, certain caveats apply here. First, some collection types cannot be passed to a Web service. For example, if you try to use a Hashtable as part of a Web service type, you'll get an exception at run time because of the way in which its elements are accessed. (You might recall the discussion in Chapter 10 about the difficulties that arise when you try to serialize a Hashtable using XML serialization.) Alternatively, you can use an ArrayList to hold your set of order items. The WsReceipt class shown below is an updated version of SimpleWsReceipt that holds a list of the items ordered (this can be checked by the client) in an ArrayList :

 packageCakeCatalogService; importSystem.Collections.ArrayList; publicclassWsReceipt { privateArrayListorderContents; /**@property*/ publicArrayListget_OrderContents() { returnorderContents; } /**@property*/ publicvoidset_OrderContents(ArrayListitems) { orderContents=items; } } 

The orderContents instance variable is exposed through the OrderContents property. Consider how this is represented in WSDL:

 <s:complexTypename="WsReceipt"> <s:sequence> <s:elementminOccurs="0" maxOccurs="1" name="CustomerName" type="s:string" /> <  s:elementminOccurs="0" maxOccurs="1" name="OrderContents" type="s0:ArrayOfAnyType" /  > <s:elementminOccurs="1" maxOccurs="1" name="OrderId" type="s:int" /> <s:elementminOccurs="1" maxOccurs="1" name="Timestamp" type="s:dateTime" /> </s:sequence> </s:complexType> 

The collection is translated into WSDL as an ArrayOfAnyType that does not contain any detailed type information. This causes a problem for the client because it does not receive information about the item data that was previously marshaled when it used an array of Items . However, the information is not lost. Examination of the SOAP request shows that this information is still in there ”you just need to do a little work to get at it:

 HTTP/1.1200OK Server:Microsoft-IIS/5.1 Date:Tue,07May200211:56:09GMT Cache-Control:private,max-age=0 Content-Type:text/xml;charset=utf-8 Content-Length:761 <?xmlversion="1.0" encoding="utf-8"?> <soap:Envelopexmlns:soap=http://schemas.xmlsoap.org/soap/envelope/ xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <SubmitBuiltUpOrderResponse xmlns="http://fourthcoffee.com/CakeCatalog/"> <SubmitBuiltUpOrderResult> <customerName>PeterWaxman</customerName> <orderId>987098</orderId> <timestamp>2002-05-07T12:56:09.1712500+01:00</timestamp>  <orderContents> <anyTypexsi:type="Item"> <itemDescription>PartyCake</itemDescription> <quantity>12</quantity> </anyType> <anyTypexsi:type="Item"> <itemDescription>WeddingCake</itemDescription> <quantity>3</quantity> </anyType> </orderContents>  </SubmitBuiltUpOrderResult> </SubmitBuiltUpOrderResponse> </soap:Body> </soap:Envelope> 

Chapter 18 will discuss how a client can retrieve this information from the SOAP response.

Polymorphism and Generic Object Conversion

Another way to approach the generic data issue is to use the XmlElement and XmlInclude attributes. Using these attributes, you can define a set of types that can be found in a given ArrayOfAnyType . When the contents of the array are marshaled as XML, the XML serializer will substitute the appropriate type into the SOAP document. For example, consider the following C# Web method that manipulates a C# version of the Order class:

 [WebMethodAttribute] [XmlInclude(typeof(Order))] publicArrayListGetMyOrder() { ArrayListorders=newArrayList(); Orderorder=newOrder(); order.CustomerName= "PeterWaxman"; order.OrderId=12345678; Item[]items=newItem[2]; items[0]=newItem(); items[0].ItemDescription= "WeddingCake"; items[0].Quantity=2; items[1]=newItem(); items[1].ItemDescription= "BirthdayCake"; items[1].Quantity=7; order.OrderContents=items; orders.Add(order); returnorders; } 

The return type of the method is completely generic. Again, this means that the WSDL generator does not know precisely what types to expect. You can help the WSDL generator out here by using the XmlInclude attribute to indicate the types you expect to be passed. The WSDL generated by this Web service defines the method's return type as an ArrayOfAnyType , but it also includes descriptions for the Order and Item types. If you access the Web service using Visual Studio .NET and call the GetMyOrder method through the test page, the XML response will be as follows:

 <?xmlversion="1.0" encoding="utf-8" ?> <ArrayOfAnyTypexmlns:xsd=http://www.w3.org/2001/XMLSchema xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns="http://tempuri.org/"> <anyTypexsi:type="Order"> <CustomerName>PeterWaxman</CustomerName> <OrderId>12345678</OrderId> <OrderContents> <Item> <ItemDescription>WeddingCake</ItemDescription> <Quantity>2</Quantity> </Item> <Item> <ItemDescription>BirthdayCake</ItemDescription> <Quantity>7</Quantity> </Item> </OrderContents> </anyType> </ArrayOfAnyType> 

The anyType element is now correctly tagged as being of type Order , which makes it easier to unmarshal on the client side.

So, the only question left is, "Why is the example in C# and not J#?" The reason is that the specification of these attributes requires a statement that can be evaluated at compile time to generate a .NET Framework System.Type instance. In C#, you can use the typeof operator to obtain one of these. However, the equivalent operator in J# returns a java.lang.Class instance, which is not altogether helpful and cannot be processed in the same way by the .NET Framework. The code that shows this example in operation is in the sample project CSTestWebService.

Passing DataSet Objects

As stated earlier, the conversion of complex types into XML and back again is a nontrivial task. Someone must take responsibility for identifying the complex type in transit and then perform the necessary marshaling and unmarshaling. An ADO.NET DataSet provides a high level of functionality in terms of its data-carrying capabilities and the relationships and constraints between the data. This potentially complex structure would be quite challenging to express in terms of a WSDL complex type. You might therefore conclude that it would be difficult to pass a DataSet across a Web service, but this is not the case.

DataSet objects can be serialized by the XmlSerializer to create an on-the-wire representation. This is not really surprising because you learned in Chapter 5 that DataSet objects lead a double life ”as both relational data holders and XML documents. The pleasant surprise is that given an XML representation of a DataSet , the .NET Framework can reconstitute the DataSet at the receiving end.

The following code from the Catalog.asmx.jsl sample file shows the creation of a DataSet in response to a request:

 /**@attributeWebMethodAttribute()*/ publicDataSetRetrieveCatalog() { DataSetds=newDataSet("FourthCoffeeCatalog"); ds.ReadXml(get_Server().MapPath("CakeCatalog.xml")); returnds; } 

The SOAP response from this request takes the following form:

 HTTP/1.1200OK Server:Microsoft-IIS/5.1 Date:Wed,24Apr200213:59:17GMT Cache-Control:private,max-age=0 Content-Type:text/xml;charset=utf-8 Content-Length:3710 <?xmlversion="1.0" encoding="utf-8"?> <soap:Envelopexmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <RetrieveCatalogResponsexmlns="http://fourthcoffee.com/CakeCatalog/"> <RetrieveCatalogResult>  <xs:schemaid="CakeCatalog" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema"  xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xs:elementname="CakeCatalog" msdata:IsDataSet="true" msdata:Locale="en-GB">  <xs:complexType> <xs:choicemaxOccurs="unbounded"> <xs:elementname="CakeType"> <xs:complexType> <xs:sequence> <xs:elementname="Message" type="xs:string" minOccurs="0" msdata:Ordinal="0" /> <xs:elementname="Description" type="xs:string" minOccurs="0" msdata:Ordinal="1" /> <xs:elementname="Sizes" minOccurs="0" maxOccurs="unbounded"> <xs:complexType> <xs:sequence> <xs:elementname="Option" minOccurs="0" maxOccurs="unbounded"> <xs:complexType> <xs:attributename="value" type="xs:string" /> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> <xs:attributename="style" type="xs:string" /> <xs:attributename="filling" type="xs:string" /> <xs:attributename="shape" type="xs:string" /> </xs:complexType> </xs:element> </xs:choice> </xs:complexType> </xs:element> </xs:schema>  <diffgr:diffgramxmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"> <CakeCatalogxmlns="">  <CakeTypediffgr:id="CakeType1" msdata:rowOrder="0" diffgr:hasChanges="inserted" msdata:hiddenCakeType_Id="0" style="Celebration" filling="sponge" shape="round"> <Message>HappyBirthday</Message> <Description>Oneofourmostpopularcakes</Description> <Sizesdiffgr:id="Sizes1" msdata:rowOrder="0" diffgr:hasChanges="inserted" msdata:hiddenSizes_Id="0" msdata:hiddenCakeType_Id="0"> <Optiondiffgr:id="Option1" msdata:rowOrder="0" diffgr:hasChanges="inserted" value="10inch" msdata:hiddenSizes_Id="0" /> <Optiondiffgr:id="Option2" msdata:rowOrder="1" diffgr:hasChanges="inserted" value="12inch" msdata:hiddenSizes_Id="0" /> <Optiondiffgr:id="Option3" msdata:rowOrder="2" diffgr:hasChanges="inserted" value="14inch" msdata:hiddenSizes_Id="0" /> </Sizes> </CakeType> </CakeCatalog> </diffgr:diffgram> </RetrieveCatalogResult> </RetrieveCatalogResponse> </soap:Body> </soap:Envelope> 

Although this listing was simplified to save space, it is still quite complex. The main points to extract from it are:

  • The SOAP message contains an XML schema that defines the format of the data.

  • The definition for the CakeCatalog element declares that it is a DataSet , using the msdata:IsDataSet="true" attribute.

  • The data is contained in a DiffGram . (See Chapter 5 for more information on DiffGram elements).

  • If the .NET Framework receives this message, it will have enough information to reconstitute the DataSet .

  • If any other platform receives the message, it can use the schema definition to understand the data contained in the message and manipulate it accordingly .

Returning to the topic of designing XML Web services, you might need to retrieve data from a database, such as a list of customers, and return this to the client. One solution is to create a set of domain objects based on this data, such as an array of instances of a Customer class, and then pass these domain objects back from your Web service call. This will generate "clean" XML descriptions that can be consumed equally well by clients on any platform. However, if you use a DataSet (or, even better, a typed DataSet ) to hold your list of customers, the XmlSerializer will generate the XML description for you. This description can be reconstituted into the typed DataSet on a .NET client or handled based on its schema in a non-.NET client. Given that you'll frequently retrieve database data in a DataSet , you should consider whether you really want to convert this data into domain object types or whether you should just pass back the DataSet and let the XmlSerializer generate the XML for you.

Passing XML Documents

One decision you must make when you design a Web service is what style of calls should be made between the client and server. As noted previously, the fact that SOAP originates from an initiative to run RPC over HTTP means that most of the focus is on RPC-style interaction. The alternative is to pass XML documents. However, you must consider whether you want to abandon language-specific types altogether and just pass a single XML document as a parameter to a Web method. This type of message-oriented exchange offers several advantages over the RPC-style interaction:

  • You can define a schema for the document to verify the precise structure of its contents.

  • You won't encounter any issues regarding the mapping of domain types to XML because the data is already represented as XML.

  • You won't need to build and maintain ongoing state between client and server. (RPC-style calls tend to imply a certain amount of state.) All the information required to process the document can be carried with it.

  • The interface of your service is far more flexible in the face of change. Because the signature of the call between client and server consists of a single parameter (the XML document) in which all the data is encoded, changes to that data will affect only the schema of the document passed. The signature of the method on your service will remain the same.

However, message-oriented exchanges also have some disadvantages:

  • The validation of the document using a schema requires an extra step.

  • You must use XML mechanisms, such as DOM, to manipulate your data rather than language-level mechanisms such as typed variables.

  • If the processing of the document fails early, a lot of unnecessary data will have been passed.

  • By having a single, generic parameter rather than multiple, specific parameters, you lose a lot of the type safety associated with interface-based remote interaction.

  • Document-oriented interactions are more difficult to describe in WSDL.

If you define a method that returns an XmlDocument and attach the WebMethod attribute to this method, the generated WSDL will allow almost any content. The following is the WSDL generated for the RetrieveXmlCatalog method defined in the sample file Catalog.asmx.jsl. This method returns an XmlDocument . The any type in WSDL indicates any XML-compliant content.

 <s:elementname="RetrieveXmlCatalogResponse"> <s:complexType> <s:sequence> <s:elementminOccurs="0" maxOccurs="1" name="RetrieveXmlCatalogResult"> <s:complexTypemixed="true"> <s:sequence>  <s:any/>  </s:sequence> </s:complexType> </s:element> </s:sequence> </s:complexType> </s:element> 

The return value from the RetrieveXmlCatalog method will appear on the client as an XMLNode object that can be manipulated as appropriate, as you'll see in Chapter 18.

I l @ ve RuBoard


Microsoft Visual J# .NET (Core Reference)
Microsoft Visual J# .NET (Core Reference) (Pro-Developer)
ISBN: 0735615500
EAN: 2147483647
Year: 2002
Pages: 128

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net