5.                        Chapter 5 The Microsoft SOAP Toolkit

Most of the time you’ll use development tools to help you expose and invoke Web services. This chaper focuses on the Microsoft SOAP Toolkit as an example of such a tool. The toolkit is COM-based and can be used with any COM-capable programming language including Visual Basic 6.0, VBScript, and Visual C++. This chapter explains the toolkit’s components and architecture then shows you how to use it to expose and invoke Web services.

Toolkit API Architecture

The SOAP toolkit is made up of two main parts that can be used together or independently. The first part is the client, which is made up of several COM components that allow you to invoke SOAP-based Web services. The second part is made of the server components used to expose Web services, usually implemented as COM components. A client application calls a Web service using the SOAP toolkit’s client API as shown in figure 5-1. The toolkit’s client reads the WSDL describing the Web service and handles creating the SOAP message and sending it in an HTTP request to the Web server. On the server, you can choose to use an ASP page or an ISAPI extension as the listener for incoming requests. This listener captures the request and invokes the toolkit’s SoapServer30 component which handles instantiating the appropriate COM component and invoking the requested method. SoapServer30 relies on information in a .wsml file to determine the ProgID of the component to instantiate and the DispID of the method to call. WSML is a toolkit-specific XML file format for providing additional information needed to invoke the Web service. After invoking the COM component’s method, SoapServer30 then takes the return value and any ByRef parameters and creates the response SOAP message. The Web server then sends this response message back to the client as the HTTP response.

Figure 5‑1

Data flow through the SOAP Toolkit.

 

The toolkit provides a high-level API which makes it almost trivial to expose and invoke Web services by encapsulating all the SOAP message creation and parsing. For cases where you need more control, the toolkit also exposes a low-level API that allows you to control every aspect of SOAP messages and therefore requires extra coding. This high level of control sometimes comes in handy when trying to interoperate with Java-based and other Web services.

 

Exposing Web Services

From a business standpoint, there are great benefits to be gained by exposing existing COM-based server components as Web services. The most obvious benefit is easier integration with other platforms. As explained in chapter 1, most business functions today span multiple supporting applications running on several platforms. Integrating those applications can improve the overall business process by removing or reducing some of the seams and gaps in existing solutions.

Businesses can also benefit from exposing existing data to other applications rather than people. In the past 6 to 7 years there’s been an explosion of intranet applications designed to make business information readily available to more workers. A typical Web application’s architecture is based on a COM-based middle tier with an ASP based front end. If this describes your applications, you’ve already done the hard part: You’ve created business logic and data access code in the form of COM components. To extend the reach of your application, you can expose these existing components (or some of them) as Web services for consumption by other applications rather than people. Doing this increases the usefulness of existing multi-tier applications and allows other systems to nicely integrate with your systems (back to integration again).

Finally, a huge business benefit is the ease of deployment of client applications. Again, if you’ve built the server COM components, you’ve already taken care of the server side of things. Exposing these components as Web services makes it easier to deploy clients that require only HTTP access to the server. Simplifying deployment and network configuration translates into saved time (i.e. money) and frustration. So how do you expose existing COM components as Web services?

It’s generally a bad idea to try to expose each COM component as a Web service and each interface method as a Web service operation. Instead, you should design a new layer on top of existing server-side (e.g. MTS or COM+) components as in figure 5-2. This Web Services Layer (WSL) is a new set of stateless COM components designed to expose the services that clients need in order to perform their business function.

When designing the WSL, think in terms of business services and documents not in terms of object models. For example, don’t design an Order object with properties such as order number, and order amount and methods such as Save, Update, and Delete. Instead, design an OrderMgmt Web service with PlaceOrder and DeleteOrder operations[1] that take in an Order XML document and return an OrderReceipt document. Use XML Schemas to define each of your service documents. Each of the service operations, e.g. PlaceOrder, would aggregate several existing COM components to perform a specific, complete, business function.

The WSL uses the SOAP Toolkit to handle incoming SOAP requests and send outgoing responses. You can use either the high level API or the low level API to do this. I usually recommend using the low level API because it is a consistent API that gives you lots of control over the messages being transmitted. However, there are many cases where the high level API might be sufficient, especially if you are building quick prototypes. I will first explain the use of the high level API for exposing and invoking Web services then I’ll show you how the low level API works.

Figure 5‑2

Creating a Web Services Layer to expose existing COM components

 

 

Using the High-Level API

Exposing Web Services

Now that you understand when and why you’d expose COM components and Web services, let’s examine the how. This section covers the mechanics of exposing COM components using the SOAP Toolkit’s high level API.

To expose a COM component, you need a WSDL document that describes the Web service operations as explained in chapter 4. When using the high level API, you also need a WSML document which maps services to COM components and service operations to interface methods. Instead of creating these documents manually, you use the WSDL Generator that comes with the toolkit[2]. This wizard reads a COM component’s type library and lets you choose which methods you want to expose. It then generates the necessary documents based on information in the type library.

Consider the simple component in listing 5-1, with one method called GetStoreSales which returns a sales figure given a store id. Let’s walk through the steps of exposing it as a Web service.

 

Listing 5‑1 A GetStoreSales method that is to be exposed as a Web service operation (VBWSBook\Chapter5\HLServer\Stores.cls)

Option Explicit

Private Const UID As String = "admin"

Private Const PWD As String = "admin"

Public Function GetStoreSales(ByVal user As String, _

            ByVal password As String, _

            ByVal StoreId As String) As Double

    If user = UID And password = PWD Then

        Select Case StoreId

            Case "6380"

                GetStoreSales = 120490.87

            Case "7066"

                GetStoreSales = 190100.04

            Case "7896"

                GetStoreSales = 115900.56

        End Select

    Else

        GetStoreSales = 0

    End If

End Function

When you launch the wizard, it first asks you whether you want to use an existing configuration file. When you use the wizard you can save your selections and input to a configuration file then use this file as a starting point the next time you run the wizard. Since this is the first time you run the wizard, you won’t use an existing configuration file. On the next screen you enter a Web service name and the COM component’s path as in figure 5-3. On the next screen, you pick from a list of components and methods available in the selected .dll.

 

Figure 5‑3

Specifying service name and selecting a .dll.

 

When you click next, you get a screen asking you to specify SOAP listener information. You now get to decide on the type of listener: ASP or ISAPI. Generally, an ISAPI listener is slightly faster than an ASP listener. However, you can learn more from examining the code within an ASP listener, so go ahead and select ASP listener for this example. Later in this chapter, I will explain how the ISAPI listener works and how to use it.

 

You must also specify the listener’s URL. Here’s a tip to ensure you enter the right URL here. First, create a folder on your hard drive where you want the listener and other service files to reside, e.g. D:\VBWSBook\HLServer\ or something like that. Then open the IIS administration console and create a new virtual directory called HLServer that maps to the folder you created be sure to enable directory browsing. To ensure you configured things right, open Internet Explorer and enter the URL to this new virual directory, e.g. http://VBWSServer/HLServer. If everything is right, you should see a directory listing of the folder you created which is probably empty at this point.


Now copy the URL from IE’s address bar and past it in the wizard’s URI field as shown in figure 5-4. When you click next, you get the screen in figure 5-5 asking you to specify four URI’s. The first URI is the WSDL target namespace (see chapter 4). The second one is the schema target namespace (see chapter 2). The third URI is the SOAP operation namespace (see chapter 3). Finally, the last URI is the value of the SOAPAction HTTP header (see chapter 3).

Figure 5‑4

Specifying the listener type and URL.

 

Figure 5‑5

Specifying URIs used in the service’s WSDL.

 

If you’re building a new Web service (as opposed to implementing a pre-defined Web service interface), the URI values are completely up to you. Just be consistent in what you pick and try to use your organization’s Internet domain name if possible.

The next screen (figure 5-6) asks you to pick the character set to be used for SOAP messages. For maximum interoperability, you should pick UTF-8 unless you know you’ll need to use characters from the UTF-16 character set. You also have to enter two paths. The first is where the generated WSDL, WSML, and ASP files will be saved. This must be the same folder you created and configured as a virtual directory (e.g. D:\VBWSBook\HLServer\). The second path is where the WSDL Generator’s configuration file will be saved. You can choose any folder to store the configuration file and it need not be Web-accessible.

Figure 5‑6

Sepcifying location to store configuration and output files. Output file location must correspond to the service URL specified earlier.

 

When you click next, the wizard generates the three files and saves them to the specified folder. Listing 5-2 shows an example generated WSDL document.

 

Listing 5‑2 A WSDL document generated by the wizard. (VBWSBook\Chapter5\HLServer\HLServer.WSDL)

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

<definitions

 name="HLServer"

 targetNamespace="http://LearnXmlWS.com/HLServer/wsdl/"

 xmlns:wsdlns="http://LearnXmlWS.com/HLServer/wsdl/"

 xmlns:typens="http://LearnXmlWS.com/HLServer/type/"

 xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"

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

 xmlns:stk="http://schemas.microsoft.com/soap-toolkit/wsdl-extension"

 xmlns="http://schemas.xmlsoap.org/wsdl/">

  <types>

    <schema

      targetNamespace="http://LearnXmlWS.com/HLServer/type/"

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

      xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"

      xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"

      elementFormDefault="qualified"/>

  </types>

  <message name="Stores.GetStoreSales">

    <part name="user" type="xsd:string"/>

    <part name="password" type="xsd:string"/>

    <part name="StoreId" type="xsd:string"/>

  </message>

  <message name="Stores.GetStoreSalesResponse">

    <part name="Result" type="xsd:double"/>

  </message>

  <portType name="StoresSoapPort">

    <operation

             name="GetStoreSales"

             parameterOrder="user password StoreId">

      <input message="wsdlns:Stores.GetStoreSales"/>

      <output message="wsdlns:Stores.GetStoreSalesResponse"/>

    </operation>

  </portType>

  <binding name="StoresSoapBinding" type="wsdlns:StoresSoapPort">

    <stk:binding preferredEncoding="UTF-8"/>

    <soap:binding

          style="rpc"

          transport="http://schemas.xmlsoap.org/soap/http"/>

    <operation name="GetStoreSales">

      <soap:operation

    soapAction="http://LearnXmlWS.com/HLServer/action/Stores.GetStoreSales"/>

      <input>

        <soap:body use="encoded"

                   namespace="http://LearnXmlWS.com/HLServer/message/"

                   encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"

                   parts="user password StoreId"/>

      </input>

      <output>

        <soap:body use="encoded"

            namespace="http://LearnXmlWS.com/HLServer/message/"

            encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"

            parts="Result"/>

      </output>

    </operation>

  </binding>

  <service name="HLServer">

    <port name="StoresSoapPort"

          binding="wsdlns:StoresSoapBinding">

      <soap:address

         location=

"http://VBWSServer/VBWSBook/Chapter5/HLServer/HLServer.ASP"/>

    </port>

  </service>

</definitions>

The generated WSDL contains no type definitions because the only method we exposed uses simple types e.g. strings and double. There are two messages in this WSDL, Stores.GetStoreSales and Stores.GetStoreSalesResponse. There’s also one operation named GetStoreSales and one SOAP binding named StoresSoapBinding. This SOAP binding uses RPC/encoded messages as indicated by the style and use attributes in listing 5-2. The service end point URL is specified in the <soap:address> location attribute. This URL points to the generated ASP page because you chose to use an ASP listener.

Listing 5-3 shows the WSML document generated for the example HLServer.

 

Listing 5‑3 A WSML document generated by the wizard. (VBWSBook\Chapter5\HLServer.WSML)

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

<servicemapping name="HLServer">

  <service name="HLServer">

    <using PROGID="HLServer.Stores" cachable="0" ID="StoresObject"/>

    <port name="StoresSoapPort">

      <operation name="GetStoreSales">

        <execute uses="StoresObject"

                 method="GetStoreSales" dispID="1610809344">

          <parameter callIndex="-1" name="retval" elementName="Result"/>

          <parameter callIndex="1" name="user" elementName="user"/>

          <parameter callIndex="2" name="password" elementName="password"/>

          <parameter callIndex="3" name="StoreId" elementName="StoreId"/>

        </execute>

      </operation>

    </port>

  </service>

</servicemapping>

A WSDML document provides the information necessary to map a SOAP request message to the corresponding component and method. The one in listing 5-3 provides a mapping for the HLServer service as indicated by the <service> name attribute. The specific COM component exposing this service is identified by the <using> element’s PROGID attribute. Each Web service operation defined in the WSDL document has a corresponding <operation> element in the WSML document which specifies the method name, dispID and parameters. Note that you must regenerate the WSML document if you change the dispID of one or more methods e.g. by reordering methods on your VB classes.

Listing 5-4 shows the generated ASP page which acts as the Web service listener.

 

Listing 5‑4 An ASP listener generated by the wizard. (VBWSBook\Chapter5\HLServer\HLServer.asp)

<%@ LANGUAGE=VBScript %>

<%

Option Explicit

On Error Resume Next

Response.ContentType = "text/xml"

Dim SoapServer

If Not Application("HLServerInitialized") Then

  Application.Lock

  If Not Application("HLServerInitialized") Then

    Dim WSDLFilePath

    Dim WSMLFilePath

    WSDLFilePath = Server.MapPath("HLServer.wsdl")

    WSMLFilePath = Server.MapPath("HLServer.wsml")

    Set SoapServer = Server.CreateObject("MSSOAP.SoapServer30")

    If Err Then SendFault "Cannot create SoapServer object. " & _

       Err.Description

    SoapServer.Init WSDLFilePath, WSMLFilePath

    If Err Then SendFault "SoapServer.Init failed. " & Err.Description

    Set Application("HLServerServer") = SoapServer

    Application("HLServerInitialized") = True

  End If

  Application.UnLock

End If

Set SoapServer = Application("HLServerServer")

SoapServer.SoapInvoke Request, Response, ""

If Err Then SendFault "SoapServer.SoapInvoke failed. " & Err.Description

 

Sub SendFault(ByVal LogMessage)

  Dim Serializer

  On Error Resume Next

  ' "URI Query" logging must be enabled for AppendToLog to work

  Response.AppendToLog " SOAP ERROR: " & LogMessage

  Set Serializer = Server.CreateObject("MSSOAP.SoapSerializer30")

  If Err Then

    Response.AppendToLog "Could not create SoapSerializer30 object. " & _

                         Err.Description

    Response.Status = "500 Internal Server Error"

  Else

    Serializer.Init Response

    If Err Then

      Response.AppendToLog "SoapSerializer.Init failed. " & Err.Description

      Response.Status = "500 Internal Server Error"

    Else

      Response.Status = "500 Internal Server Error"

      Serializer.startEnvelope

      Serializer.startBody

      Serializer.startFault "Server", _

          "The request could not be processed due to a " & _

          "problem in the server. Please contact the " & _

          "system admistrator. " & LogMessage

      Serializer.endFault

      Serializer.endBody

      Serializer.endEnvelope

      If Err Then

        Response.AppendToLog "SoapSerializer failed. " & Err.Description

        Response.Status = "500 Internal Server Error"

      End If

    End If

  End If

  Response.End

End Sub

%>

Most of the code in listing 5-4 handles initialization and errors, the code that does the real work is only a few lines. The page first sets the response type to text/xml as required by SOAP messages. It then reads a flag called HLServerInitialized out of the ASP Application object. This flag is used to determine whether there’s already a cached version of the SoapServer30 object that can be used to process this request. The first time you call the service, this flag will be false, so the code will lock the Application then obtain the WSDL and WSML file path’s by using Server.MapPath. Note that the assumption here is that both files reside in the same folder as this ASP page. if that’s not the case you need to change the parameter to Server.MapPath or set WSDLFilePath and WSMLFilePath to the right file paths directly.

The next line creates a SoapServer30 object which will process this and future incoming requests. If an error occurs, a SOAP Fault is returned to the client. The procedure called SendFault in listing 5-4 handles the details of constructing the SOAP Fault using a SoapSerializer30 object (more on this object later) then calls Response.End to end execution. If the SoapServer object was created successfully, the page calls Init passing it the WSDL and WSML file paths. It then adds the initialized SoapServer to the ASP Application object as an item called HLServerServer (name of your service with the word Server appended) and sets the HLServerInitialized flag to true and unlocks the Application. Subsequent calls to this service will be processed by the same instance of SoapServer30.

If you make changes to the service and regenerate the WSDL and WSML files you need to stop and restart the IIS application. Otherwise clients will continue to use the cached instance of SoapServer30 that was initialized with the older WSDL and WSML documents.

Now the real work happens, the page calls SoapServer30.SoapInvoke passing it the ASP Request and Response objects. The third parameter to SoapInvoke is an optional default value for the SOAPAction header. Since the incoming request should have a SOAPAction header, SoapServer30 can read it directly from the ASP Request object so no default value is needed.

Calling SoapServer.SoapInvoke does all the real work. It reads the incoming SOAP message, finds out which COM component and method are requested, deserializes incoming data into the right types, invokes the desired method and serializes the return value and sends the response SOAP message. Figure 5-7 shows a pseudo sequence diagram of the interaction between the client, ASP listener, SoapServer30 and your server COM component.

Figure 5‑7

A pseudo sequence diagram showing interaction between an ASP listener, an instance of SoapServer30 and the service’s COM component.

 

 

You can modify the ASP listener page to suit your needs. For example, you can make it send you an email if certain type of error occurs while processing a request. If you do modify this page, be careful not to replace it if you re-run the WSDL generator.

 

Invoking Web Services

When you use the high-level API to invoke a Web service, your client application will primarily use a SoapClient30 object shown in figure 5-8. You might also create a class that implements IHeaderHandler to send and/or receive SOAP headers from the Web service.

 

Figure 5‑8

The client-side high-level API.

 

To invoke a Web service you first instantiate a SoapClient30 object and call its MSSoapinit method passing it the URL to the service’s WSDL document as shown in listing 5-1.

 

Listing 5‑5 Using the high level API to invoke a Web service (VBWSClientCode\HLClient\frmClient.frm)

Private Sub cmdSales_Click()

    On Error GoTo eh

    Dim soap As MSSOAPLib30.SoapClient30

    Set soap = New MSSOAPLib30.SoapClient30

    'For completeness, we specify the

    'service name (HLServer) and

    'the port name (StoresSoapPort)

    'but they are optional when

    'the WSDL doc contains

    'one service with one SOAP port

    soap.MSSoapInit WSDL_URL, "HLServer", "StoresSoapPort"

   

    'the following 3 lines use a proxy

    'For example you can use ProxyTrace

    'for troubleshooting

    If chkProxy.Value Then

        soap.ConnectorProperty("ProxyServer") = "localhost"

        soap.ConnectorProperty("ProxyPort") = "8080"

        soap.ConnectorProperty("UseProxy") = True

    End If

 

   Dim Sales As Double

   'this is the Web service call

   Sales = soap.GetStoreSales(UID, PWD, cboStoreId.Text)

    lblSales.Caption = "Sales for this store are: $" & CStr(Sales)

    Exit Sub

eh:

   Dim msg As String

   msg = Err.Description & "Fault Code: " & soap.FaultCode

   msg = msg & vbCrLf & "Fault String: " & soap.FaultString

   msg = msg & vbCrLf & "Fault Detail: " & soap.Detail

   MsgBox msg, vbCritical, "Error calling service"

       

End Sub

 

After calling MSSoapinit, you then call the Web service operation as if it were a method on SoapClient30 object. The example in listing 5-1 calls an operation named GetStoreSales which takes in a user id, a password, and the store id and returns the monthly sales for that store. Note that SoapClient30 doesn’t actually have a GetStoreSales method, so you will not get this method in intellisense, nor will the compiler catch errors like passing a wrong number of arguments or arguments of the wrong type.

SoapClient30 exposes a ConnectorProperty that lets you set various HTTP-related properties. For example, listing 5-5 shows an example of using ConnectorProperty to configure a proxy server. The SOAP Toolkit will automatically use the same HTTP proxy settings that you have configured in Internet Explorer’s connection settings. Therefore, you don’t need to explicitly set ProxyServer and ProxyPort unless you don’t want to use the same settings as IE, but you do need to set UseProxy to True.

Error Handling

When an error occurs you can use SoapClient30 properties to get information from the SOAP Fault element. SoapClient30 exposes Fault information in five properties: FaultActor, FaultCode, FaultCodeNamespace FaultString and Detail. Listing 5-5 shows how you can read some of this information in an error handler and present it to the user.

Troubleshooting with the Trace Utility

When things go wrong and you want to troubleshoot your client and/or service, I recommend you start by viewing request/response messages to determine if everything is being sent as you expect it. The SOAP Toolkit comes with a handy trace utility that can capture and display HTTP requests as shown in figure 5-9.

 

Figure 5‑9

Using the trace utility to capture and display SOAP messages.

 

The trace utility runs on the same machine as the client and listens on the port that you configure, e.g. port 8080. Instead of sending requests directly to the service, the client sends all requests to the trace utility on the local host. Upon receiving a request, the trace utility forwards it to the service which can be on any host (not necessarily the localhost). The service sends back an HTTP response and the trace utility forwards that response back to the client.

To display captured messages, you can choose a formatted trace which displays nicely formatted XML using Internet Explorer’s default stylesheet as in figure 5-10. Or you can choose an unformatted trace which displays message content in hex as shown in figure 5-11.

 

Figure 5‑10

A formatted trace captured with the toolkit’s trace utility.

Figure 5‑11

An un-formatted trace displayed request and response messages in hex.

 

The trick is getting the client to send messages to the trace utility rather than the service. By default, the client sends requests to the URL specified in the WSDL port element. To override this URL, you can use the EndPointURL connector property to specify the trace utility’s URL. For example:

 

soap.ConnectorProperty("EndPointURL") = _  "http://localhost:8080/VBWSBook/Chapter5/HLServer/HLServer.asp"

 

You want to do this after you call MSSoapInit but before you begin calling Web service operations.

 

Using High-Level API From Classic ASP

To invoke Web services from a classic ASP page, you use the SoapClient30 object much like a VB client would. There’s only one extra step you need to be aware of: If you use HTTP to load the WSDL file, you must set the ServerHTTPRequest client property to True. Listing 5-6 shows an example ASP page that calls a Web service using the high-level API.

 

Listing 5‑6 An example ASP page inoking a Web service with the SOAP toolkit. (VBWSBook\Chapter5\weather.asp)

<%

    Dim zipCode

    zipCode=Request.QueryString("zip")

    Dim soap

    Set soap = Server.CreateObject("MSSOAP.SoapClient30")

    soap.ClientProperty("ServerHTTPRequest")=True

    soap.MSSoapInit _

       "http://www.learnxmlws.com/services/weatherRetriever.asmx?wsdl"

   

    Dim temperature

    temperature=soap.GetTemperature(zipCode)

%>

<html>

<body>

<p>The temperature at <% =zipCode %> is <b><% =temperature %></b> degrees</p>

</body>

</html>

There are two interesting things in listing 5-6. First notice that you create the client using Server.CreateObject with the ProgID MSSOAP.SoapClient30.  Also, notice the line

 

   soap.ClientProperty("ServerHTTPRequest")=True

 

This line is required because the WSDL document is loaded using HTTP as indicated by the URL used with MSSoapInit. Note that when you set ServerHTTPRequest to True, you must use ProxyCfg, the WinHTTP Proxy Configuration tool, to configure proxy settings even if you are not using a proxy. You can download this tool from http://msdn.microsoft.com/msdn-files/027/001/468/Proxycfg.exe.

 

Serialization in the High-Level API

The high level API abstracts your application from the underlying SOAP messages. It could also abstract your application from dealing with XML by automatically serializing application data to XML and deserializing XML into application data on the other end. The example in listing 5-5 was using only simple types, e.g. strings and doubles which are easier to serialize than complex types. The SOAP toolkit also supports complex type serialization with built-in type mappers and the ability to create your own custom type mappers.

You can skip complex type serialization altogether by passing/returning XML nodes instead of objects to/from Web service operations. Even with the high level API, the SOAP toolkit lets you do this by passing or returning an IXMLDOMNodeList which is a list of DOM nodes. This approach has many benefits and I recommend using it whenever you can. I’ll explain the details of using this approach and its benefits later in this section. First, I’ll quickly give you an overview of other options for serializing complex types.

 

Generic Type Mapper

The generic type mapper handles serialization to/from application objects. It serializes object properties to XML elements and deserializes XML elements back into object properties. Consider a server COM component with the GetStoreSales method shown in listing 5-7.

 

Listing 5‑7 A GetSoreSales method that returns a Store object. (VBWSBook\Chapter5\ComplexTypeServer\Stores.cls)

Public Function GetStoreSales(ByVal user As String, _

            ByVal password As String, _

            ByVal StoreId As String) As Store

    If user = UID And password = PWD Then

        Dim objStore As Store

        Set objStore = New Store

        Select Case StoreId

            Case "6380"

                objStore.Sales = 120490.87

            Case "7066"

                objStore.Sales = 190100.04

            Case "7896"

                objStore.Sales = 115900.56

        End Select

    Else

        objStore.Sales = 0

    End If

    Set GetStoreSales = objStore

End Function

This simple method instantiates a new Store object using the Store class shown in listing 5-8. It then sets properties of this object and returns it to the caller.

 

Listing 5‑8 The Store class. (VBWSBook\Chapter5\ComplexTypeServer\Store.cls)

'store class

Private mvarStoreId As String 'local copy

Private mvarSales As Double 'local copy

Private mvarIncome As Double 'local copy

Public Property Let Income(ByVal vData As Double)

    mvarIncome = vData

End Property

Public Property Get Income() As Double

    Income = mvarIncome

End Property

Public Property Let Sales(ByVal vData As Double)

    mvarSales = vData

    Me.Income = vData * 0.7

End Property

Public Property Get Sales() As Double

    Sales = mvarSales

End Property

Public Property Let StoreId(ByVal vData As String)

    mvarStoreId = vData

End Property

Public Property Get StoreId() As String

    StoreId = mvarStoreId

End Property

There’s nothing earth shattering about the Store class, it’s a simple class with a few properties. The Sales property Let procedure also sets the income property to 70% of sales (pretty healthy income). When you run the WSDL Generator wizard to expose this server component, the generated WSDL will include a complex type definition corresponding to the Stores class. The complex type is named after the COM interface name, i.e. _Store as shown in listing 5-9.

There are two limitations you need to be aware of when using the generic type mapper: First, it always maps object properties to XML elements and vice versa. Therefore there must be a one-to-one correspondence between elements in the SOAP message and properties on your application objects, XML attributes are not supported. Second, the generic type mapper does not support object structures with loops in them. For example, if you have an object graph where object A references object B reference object C which in turn references object A the generic mapper wouldn’t know how to handle this. By tracing the object references, the generic mapper would get into an infinite loop while trying to serialize such structure.

 

Listing 5‑9 The WSDL types section defining a _Store type. (VBWSBook\Chapter5\ComplexTypeServer\ComplexTypeServer.wsdl)

  <types>

    <schema

     targetNamespace="http://LearnXmlWS.com/ComplexTypeServer/type/"

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

     xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"

     xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"

     elementFormDefault="qualified">

      <complexType name="_Store">

        <sequence>

          <element name="Income" type="double"/>

          <element name="Sales" type="double"/>

          <element name="StoreId" type="string"/>

        </sequence>

      </complexType>

    </schema>

</types>

The wizard does a good job of mapping object properties to their corresponding XSD types as shown in listing 5-9. It also creates two WSML files, one for server and one for client use. Both files contain identical <types> sections which define how to handle the _Store complexType as shown in listing 5-10.

 

Listing 5‑10 Both server and client WSML files contain identical <types> sections. (VBWSBook\Chapter5\ComplexTypeServer\ComplexTypeServer.wsml)

<types>

  <type

     name='_Store'

     targetNamespace='http://LearnXmlWS.com/ComplexTypeServer/type/'

     uses='GCTM'

     targetClassId='{CB1C1344-96A8-4C67-8AEF-383941143E0F}'

     iid='{D92E1F9B-7AEA-4ECD-AED2-5A2E2245ECAC}'/>

</types>

The type information in listing 5-10 specifies the COM CLSID and IID for the COM class that should be used to handle the complexType _Store. These ids are the same in the client and server WSML which means you must have the same COM class (i.e. Store) registered on the server and client machines. If the Store class you’ll use on the client side is different from the one used by the server, you can edit the client’s WSML file and manually enter the appropriate CLSID and IID. Make sure however that the Store class you use on the client has the same properties with the same names and data types as those on the server’s Store class.

This is one of the reasons why I recommend that you skip built-in serialization/deserialization and just use XML: You don’t need to deploy the Store class to the client. In my experience, deployment is usually a source of headaches in COM-based client applications. Anything you can do to minimize the number of components that you deploy will ultimately save you time and money.

 

Assuming you do use the generic type mapper, a client can invoke GetStoreSales and get the output as an instance of Store. As mentioned above, the Store component must be deployed and registered on client machines (you would put it in a separate DLL rather than in the same DLL as the server component). The client project would also have to add a reference to the Store component in order to compile. Listing 5-11 shows an example client calling GetStoreSales and retrieving the output as an instance of Store.

 

Listing 5‑11 A client calling GetStoreSales to retrieve a Store object. (VBWSClientCode\Chapter5\ComplexTypeClient\frmClient.frm)

Private Sub cmdSales_Click()

    On Error GoTo eh

    Dim soap As MSSOAPLib30.SoapClient30

    Set soap = New MSSOAPLib30.SoapClient30

   

    soap.MSSoapInit WSDL_URL, , , WSML_URL

 

    'the following 3 lines use a proxy

    'For example you can use ProxyTrace

    'for troubleshooting

    'replace VBWSServer with the name of your proxy server

    If chkProxy.Value Then

        soap.ConnectorProperty("ProxyServer") = "localhost"

        soap.ConnectorProperty("ProxyPort") = "8080"

        soap.ConnectorProperty("UseProxy") = True

    End If

       

    Dim objStore As Store

    'this is the Web service call

    Set objStore = soap.GetStoreSales(UID, PWD, cboStoreId.Text)

    lblSales.Caption = "Sales for this store are: $" & CStr(objStore.Sales)

    lblSales.Caption = lblSales.Caption & vbCrLf & "Income is: $" & CStr(objStore.Income)

   

    Exit Sub

eh:

    Dim msg As String

    msg = Err.Description & "Fault Code: " & soap.FaultCode

    msg = msg & vbCrLf & "Fault String: " & soap.FaultString

    msg = msg & vbCrLf & "Fault Detail: " & soap.Detail

    MsgBox msg, vbCritical, "Error calling service"

End Sub

This client is the very similar to the one in listing 5-5 that was using only simple types except it must pass the WSML document URL to the SoapClient30 MSSoapInit method. It also uses Store as the return value from GetStoreSales instead of a double. The returned object contains both store sales and income which can be accessed by the client by invoking the object’s properties.

 

User-defined Data Type Mapper

The SOAP Toolkit also has a built-in User-Defined type mapper. Like the name implies, this mapper knows how to serialize and deserialize user defined types (UDTs). Listing 5-12 shows an example server that uses a Store UDT instead of a class. Listing 5-13 shows the corresponding client code. These listings are very similar to those using a Store class (listings 5-7 and 5-11). The primary difference being the use of the Set keyword when you are setting an object reference.

Listing 5‑12 A server method returning a User Defined Type (VBWSBook\Chapter5\UDTServer\Stores.cls)

Public Type Store

     Sales As Double

     Income As Double

     StoreId As String

End Type

Public Function GetStoreSales(ByVal user As String, _

            ByVal password As String, _

            ByVal StoreId As String) As Store

 

    If user = UID And password = PWD Then

        Dim theStore As Store

        Select Case StoreId

            Case "6380"

                theStore.Sales = 120490.87

            Case "7066"

                theStore.Sales = 190100.04

            Case "7896"

                theStore.Sales = 115900.56

        End Select

    Else

        theStore.Sales = 0

    End If

    theStore.Income = theStore.Sales * 0.7

    GetStoreSales = theStore

End Function

 

Listing 5‑13 A client calling the UDTServer (VBWSClientCode\Chapter5\UDTClient\frmClient.frm)

Private Sub cmdSales_Click()

    On Error GoTo eh

    Dim soap As MSSOAPLib30.SoapClient30

    Set soap = New MSSOAPLib30.SoapClient30

   

    soap.MSSoapInit WSDL_URL, , , WSML_URL

 

    'other code removed ...

       

    Dim theStore As Store

    'this is the Web service call

    theStore = soap.GetStoreSales(UID, PWD, cboStoreId.Text)

    lblSales.Caption = "Sales for this store are: $" & CStr(theStore.Sales)

    lblSales.Caption = lblSales.Caption & vbCrLf & _

                  "Income is: $" & CStr(theStore.Income)

   

   'other code removed ...

 

End Sub

 

Custom Type Mappers

If the built-in generic and UDT mappers don’t work for you, you can build your own custom mapper. For example, if there isn’t a one-to-one correspondence between your application class’s properties and elements you might want to implement a custom type mapper.

To use a custom type mapper you must first create a COM class that implements the ISoapMapper interface. The SOAP toolkit components call methods of this interface is called at runtime asking your implementation to serialize or deserialize the custom types. To tell the SOAP toolkit about this type mapper, you need to first define a complexType in the service’s WSDL document then define your custom type mapper as the mapper for this complexType in the service’s WSML document.

I will not go into the details of how to implement custom type mappers. Instead, I recommend you use XML directly via the IXMLDOMNodeList instead of custom type mappers.

Complex types as IXMLDOMNodeList

Instead of playing the serialization/deserialization game, you could instead use XML as the native data format within your applications. This lets you easily deposit XML in outgoing SOAP messages and pull out XML from incoming messages without the need for complicated serialization/deserialization logic. In my opinion, using XML as your application’s internal data format has many benefits over using classes as data containers. Here’s a list of these benefits:

·         Less code: Even though it might seem that a client using the XML DOM to manipulate data is using a lot of code, it is usually less code than a client using classes as data containers, but you must factor in the data container code itself. For example, listing 5-14 shows a client that calls the same complexTypeServer from listing 5-7.

 

Listing 5‑14 A client invoking complexTypeServer and getting the result in an IXMLDOMNodeList. (VBWSClientCode\Chapter5\XmlClient\frmClient.frm)

Private Sub cmdSales_Click()

    On Error GoTo eh

    Dim soap As MSSOAPLib30.SoapClient30

    Set soap = New MSSOAPLib30.SoapClient30

   

    soap.MSSoapInit WSDL_URL

 

    'other code omitted

       

    Dim nl As IXMLDOMNodeList

    'this is the Web service call

    Set nl = soap.GetStoreSales(UID, PWD, cboStoreId.Text)

    Dim doc As MSXML2.DOMDocument40

    'put the returned node list in the document

    Set doc = MakeDoc(nl, "Store")

    lblSales.Caption = "Sales for this store are: $" & _

            doc.selectSingleNode("/Store/Sales").Text

    lblSales.Caption = lblSales.Caption & vbCrLf & "Income is: $" & _

            doc.selectSingleNode("/Store/Income").Text

   

    'other code omitted

End Sub

 

'MakeDoc function

Private Function MakeDoc( _

            nl As IXMLDOMNodeList, _

            elemName As String) _

                As DOMDocument40

    Dim doc As New MSXML2.DOMDocument40

    doc.LoadXml "<" & elemName & "/>"

    Dim n As IXMLDOMNode

    For Each n In nl

        Select Case n.nodeType

            Case NODE_ATTRIBUTE

                doc.documentElement.setAttribute n.nodeName, n.nodeValue

            Case NODE_ELEMENT, NODE_TEXT

                doc.documentElement.appendChild n

        End Select

       

    Next

    Set MakeDoc = doc

End Function

The interesting part about this client is that it captures the return value from GetStoreSales in an IXMLDOMNodeList. It then calls the general purpose, reusable function called MakeDoc which takes in the node list and puts the nodes from this list into a DOM document. The client then uses SelectSingleNode with XPath to extract the specific nodes that it needs.

When you compare this code with the ComplexTypeClient in listing 5-11 you’ll find this one has a few extra lines of code but this assessment is incorrect. First, note that the MakeDoc function is reusable, so it could be encapsulated in a .bas module (or even COM component) that can be reused by many clients. Second, the ComplexTypeClient in listing 5-11 relies on the Store class from listing 5-8 so you should include the number of lines of code in the Store class when comparing the two techniques.

·         Easier deployment: Beyond the number of code lines, consider ease of deployment. Using type mappers implies custom applica