Max is the author of .NET Programming with Visual C++ and can be contacted at [email protected]
While VB .NET supports web-service integration natively with Visual Studio .NET, no such built-in support exists for VB6. Consequently, you must use the Microsoft SOAP Toolkit if VB6 clients are to consume web services. In this article, I present a method and a couple of scenarios that illustrate how legacy VB6 applications can consume web services written in C++. The complete UltraMaxService web service, which I present as an example along with a sample VB6 consumer application, is available electronically; see "Resource Center" (page 5).
The easiest way to integrate XML web services with VB6 applications involves installation of Microsoft's freely available SOAP Toolkit 3.0 (http://msdn.microsoft.com/library/default.asp?url=/downloads/ list/websrv.asp). Although the SOAP Toolkit was originally designed to provide XML web-service functionality for existing COM components (that is, to convert COM components into XML web services), it can also be used for consuming virtually any web service designed from scratch.
The first SOAP Toolkit scenario involves wrapping existing COM components as web services for replacing an existing COM-server/COM-client integration with an instantly distributed web-service provider/web-service consumer architecture. In this case, you must install the Toolkit on both the server and client machines. On the server, the SOAP Toolkit provides a SOAP wrapper for registered COM components by means of the SoapServer object. On the client, the Toolkit serves as an interface between SOAP messages and COM method calls by means of the SoapClient client. The SoapServer and SoapClient objects communicate with each other by means of SOAP messages, and perform mapping of the SOAP messages into COM method calls internally. Wrapping COM components as web services usually involves running the WSDL Generator program (included with the Toolkit) to generate Web Service Description Language (WSDL) and Web Service Meta Language (WSML) files for existing registered COM components. The resulting WSDL/WSML pair can be used by the SoapClient object to consume the COM-generated web service (see Figure 1).
While the WSDL file adheres to a standardized web-service description (http://www.w3.org/TR/wsdl12/), the WSML file is a pure Microsoft extension for mapping the web-service methods (SOAP actions) described in the WSDL into COM method calls.
A second SOAP Toolkit scenario involves consuming existing XML web services. In this case, you only need to install the Toolkit on a client machine and rely on the SoapClient object for parsing, generating SOAP messages, and masquerading web-method invocations as ordinary COM method calls. Here, you should rely on the WSDL file provided by the web service itself, and a WSML file is generally not required.
Consuming Web Services in VB6
To consume web services in VB6, you must create an instance of the SoapClient object and initialize the object by invoking the mssoapinit method and specifying the URL of the web-service WSDL file. Assuming that a reference to the Microsoft Soap Type Library (mssoap1.dll) has been added to the VB6 project, Listing One consumes a C++/ATL web service with dynamically generated WSDL. The URL of the web-service WSDL file can correspond to a local fileUltraMax.wsdl or C:\Projects\UltraMax.wsdl, for instance. Once the SoapClient object is initialized, you can invoke web-service methods directly on the SoapClient object instance (Listing Two) where the LogOn is one of the UltraMax web-service methods.
Because web-service methods are resolved dynamically by parsing the web-service WSDL, these methods are not available in the dropdown list of object methods provided by the VB6 editor. Therefore, you must examine the web-service WSDL (or any other pertinent web-service documentation) for information on the available web methods and their parameters. Table 1 summarizes the SoapClient object properties and methods. Note that the SoapClient object provides data properties such as detail, faultactor, faultcode, and faultstring for conveying SOAP fault information. These properties are set when a SOAP fault occurs, which typically corresponds to a web-service or communication error. If in response to invoking a web-service method the SoapClient object receives a SOAP fault message, it raises an error, which can be caught using On Error GoTo (see Example 1). You usually get SOAP faults when calling nonexistent web-service methods or when supplying incorrect web-service parameters.
SOAP Type Mapping
Although the SOAP Toolkit enables invoking web-service methods with minimal effort, additional code is required to handle complex user-defined types. The SoapClient object automatically parses web-service method parameters corresponding to primitive types, such as String, Long, Single, Date, and so on (in VB6 terms), but represents output parameters corresponding to hierarchical data structures as XML DOM objects. For automatic parsing of complex user-defined types (UDTs), you must develop a custom type-mapper object and register it in the WSML file used to initialize a SoapClient instance.
To create a type-mapper class, you create a new VB6 COM DLL containing a:
- Simple class corresponding to a UDT being mapped.
- Type-mapper class implementing the ISoapTypeMapper interface.
The created VB6 project should reference the Microsoft Soap Type Library (mssoap1.dll) and Microsoft XML 3.0 (msxml3.dll). The ISoapTypeMapper interface allows specifying how an arbitrary data type is serialized to or deserialized from XML. Table 2 summarizes the ISoapTypeMapper interface methods.
When implementing the custom type-mapper class, you must override all four ISoapTypeMapper methods that by default perform serialization/deserialization of a single String value.
The mStringMapper member corresponding to a default ISoapTypeMapper implementation is used to serialize/deserialize individual properties of the FileInfo class, which must be serialized and deserialized in the same order.
A more sophisticated example involves a situation when a complex type contains other complex types as members. Consider the SongInfo type in Listing Four(a), which contains a property of the FileInfo type. Listing Four(b) is required for the SongInfoMapper object to serialize/deserialize SongInfo to/from XML. The SongInfoMapper class contains an additional member mFileInfoMapper of type FileInfoMapper to serialize/deserialize FileInfo structure independently. The mFileInfoMapper member must be initialized in the ISoapTypeMapper_Init method and its corresponding ISoapTypeMapper_ Init must be invoked with the same parameters as the parent ISoapTypeMapper_Init call.
When it comes to serializing the MP3Info member, the mFileInfoMapper.ISoapTypeMapper_write method is invoked to serialize the nested complex type, and the mFileInfoMapper.ISoapTypeMapper_read method is invoked for deserialization. Since the FileInfoMapper class is used explicitly in the SongInfoMapper class its methods must be declared as Public, whereas SongInfoMapper methods can be declared as Private.
When the type-mapper classes are ready, you next create a WSML file to associate the type-mapper classes with the UDTs in SOAP messages.
The main purpose of WSML files is to define how COM object methods are mapped into web methods, which is necessary only for SOAP-wrapped COM components. An additional function of a WSML file, which comes in handy for both SOAP-wrapped and generic web services, is to specify type-mapping handler classes for UDTs.
To enact type mapping for the FileInfo and SongInfo types for the UltraMaxService web service, the contents of the associated WSML file must be like Listing Five. The <servicemapping> element wraps the <service> element, which defines operation and type mapping for a particular web service. Therefore, the name attribute of the <service> element must be set to the name of a web service for which the mapping is created (that is, the same as the name in the <service> element specified in the web-service WSDL file).
The series of <using> elements describes COM objects involved in type mapping and web-method (that is, SOAP operation) mapping. The PROGID attribute of the <using> element specifies the COM object textual name, while the ID attribute specifies the object's alias, which is going to be used throughout the WSML file. The cachable attribute of the <using> element specifies whether the COM component is kept in memory as long as the corresponding SoapServer instance is in memory. The cachable attribute is ignored for apartment and single-threaded COM components, however.
The <types> section is responsible for type mapping. The section contains a series of <type> elements, with each element specifying a concrete type-mapper handler for a particular UDT. The name attribute of a <type> element specifies the type name the way it is spelled in the web-service WSDL file, while the targetNamespace attribute specifies the namespace in which the complex type is defined. The value of the targetNamespace attribute should be extracted from the web-service WSDL file and, if the complex type is defined in the web- service WSDL itself, it is usually formatted as urn:<WebServiceName>. Lastly, the uses attribute specifies the ID of a COM component used for type mapping defined in one of the preceding <using> elements.
To enact the type mapping when invoking web-service methods, the URL of the resulting WSML file must be specified as a parameter to the mssoapinit call for initializing of the SoapClient object:
Dim WebService As New MSSOAPLib.SoapClient
UltraMax.dll?Handler=GenUltraMaxWSDL, , , _
Again, the actual location of the WSML file is not important: The file can be stored either on the server along with the corresponding web-services files, or reside locally on the client side.
Session State with SOAP Headers
A principle difference between COM components and web services is that the state of a web service is not preserved between web-method calls automatically. To enable state persistence, a web service must contain some kind of built-in session-state support. Furthermore, to ensure proper discrimination between sessions originating from different clients, session identifiers must be communicated between a web service and its clients.
When programming with ASP the session ID is usually communicated via a cookie. With web services, however, it is possible to communicate session ID in the SOAP header, which is easier in our case.
To make sure that the web-service client can receive and submit session ID with each web-method call, the SoapClient interface provides the HeaderHandler property, which can be initialized with a user-defined class for generating/parsing SOAP headers implementing the IHeaderHandler interface. Table 3 lists members of the IHeaderHandler interface.
When implementing the IHeaderHandler interface, you must override all IHeaderHandler interface methods. If the session ID is communicated in a SOAP header element called m_SessionID, Listing Six is the SessionHeader class implementing the IHeaderHandler interface.
The IHeaderHandler_readHeader function is called for each data element found in the received SOAP header. Therefore, it is necessary to examine the baseName attribute of the IXMLDOMNode to make sure that the currently processed SOAP header element is the one you are interested in. It is possible to read several SOAP header elements in this function by adding more case comparisons on element baseName.
The IHeaderHandler_willWriteHeaders method should return True because it is necessary to transmit session ID back to web service for proper session identification.
The IHeaderHandler_readHeader method simply serializes the SOAP header element called m_SessionID into an appropriate section of a transmitted SOAP messageprovided the IHeaderHandler_willWriteHeaders method returns True. It is possible to serialize more than one SOAP header element in the IHeaderHandler_readHeader method by repeating the pSerializer.startHeaderElement,pSerializer.writeString, and pSerializer.endHeaderElement statements for each header element that needs to be written.
Finally, to hook up the SOAP header handler to a particular SoapClient instance one must set the HeaderHandler property of the SoapClient object to an instance of a user-defined SOAP header handler class; for instance:
Dim SOAPHeader As New SessionHeader
Set ws.HeaderHandler = SOAPHeader
At this point, any subsequent call web-method invocation enacted through the SoapClient object results in transmission of a value stored in the SessionID property of the SOAPHeader object, as well as parsing the returned SOAP message header to update the SessionID property with a new value returned by the web service (if any). Remember, that it is up to web service to initialize and maintain the session ID value on its side.
The SOAP Toolkit provides sufficient means for consuming generic web services in VB6 clients. Although the amount of work associated with implementing custom type mapping and SOAP header handling is somewhat greater than in VB.NET, which supports web services natively, the SOAP Toolkit provides a well-defined interface for tapping into the wealth of XML web services from legacy VB6 projects.
Dim WebService As New MSSOAPLib.SoapClient WebService.mssoapinit _ "http://www.UltraMax-Music.com/UltraMax.dll?Handler=GenUltraMaxWSDL"
' Log on... WebService.LogOn "MyUserName", "MyPassword"
<b>(a)</b> Public FileName As String Public Quality As Integer <b>(b)</b> Implements ISoapTypeMapper Private mStringMapper As ISoapTypeMapper Public Sub ISoapTypeMapper_Init( _ ByVal pFactory As MSSOAPLib.ISoapTypeMapperFactory, _ ByVal pSchema As MSXML2.IXMLDOMNode, _ ByVal xsdType As MSSOAPLib.enXSDType) Set mStringMapper = pFactory.getMapper(enXSDstring, Nothing) End Sub Public Function ISoapTypeMapper_read(ByVal pNode As MSXML2.IXMLDOMNode, _ ByVal bstrEncoding As String, ByVal encodingMode As _ MSSOAPLib.enEncodingStyle, ByVal lFlags As Long) As Variant Dim fi As New FileInfo Dim Node As IXMLDOMNode fi.FileName = mStringMapper.read(pNode.selectSingleNode("FileName"), _ bstrEncoding, encodingMode, lFlags) fi.Quality = mStringMapper.read(pNode.selectSingleNode("Quality"), _ bstrEncoding, encodingMode, lFlags) Set ISoapTypeMapper_read = fi End Function Private Function ISoapTypeMapper_varType() As Long ISoapTypeMapper_varType = vbObject End Function Public Sub ISoapTypeMapper_write(ByVal pSoapSerializer As _ MSSOAPLib.ISoapSerializer, ByVal bstrEncoding As String, _ ByVal encodingMode As MSSOAPLib.enEncodingStyle, _ ByVal lFlags As Long, pvar As Variant) Dim fi As New FileInfo Set fi = pvar pSoapSerializer.startElement "FileName" mStringMapper.write pSoapSerializer, bstrEncoding, encodingMode, _ lFlags, fi.FileName pSoapSerializer.endElement pSoapSerializer.startElement "Quality" mStringMapper.write pSoapSerializer, bstrEncoding, encodingMode, _ lFlags, fi.Quality pSoapSerializer.endElement End Sub
<b>(a)</b> Public SongTitle As String Public AlbumTitle As String Public Duration As Single Public MP3Info As FileInfo <b>(b)</b> Implements ISoapTypeMapper Private mStringMapper As ISoapTypeMapper Private mFileInfoMapper As FileInfoMapper Private Sub ISoapTypeMapper_Init( _ ByVal pFactory As MSSOAPLib.ISoapTypeMapperFactory, _ ByVal pSchema As MSXML2.IXMLDOMNode, _ ByVal xsdType As MSSOAPLib.enXSDType) Set mStringMapper = pFactory.getMapper(enXSDstring, Nothing) Set mFileInfoMapper = New FileInfoMapper mFileInfoMapper.ISoapTypeMapper_Init pFactory, pSchema, xsdType End Sub Private Function ISoapTypeMapper_read(ByVal pNode As MSXML2.IXMLDOMNode, _ ByVal bstrEncoding As String, ByVal encodingMode As _ MSSOAPLib.enEncodingStyle, ByVal lFlags As Long) As Variant Dim si As New SongInfo Dim Node As IXMLDOMNode si.SongTitle = mStringMapper.read(pNode.selectSingleNode("SongTitle"), _ bstrEncoding, encodingMode, lFlags) si.AlbumTitle = mStringMapper.read(pNode.selectSingleNode("AlbumTitle"), _ bstrEncoding, encodingMode, lFlags) si.Duration = mStringMapper.read(pNode.selectSingleNode("Duration"), _ bstrEncoding, encodingMode, lFlags) Set si.MP3Info = mFileInfoMapper.ISoapTypeMapper_read(pNode.selectSingleNode("MP3Info"), _ bstrEncoding, encodingMode, lFlags) Set ISoapTypeMapper_read = si End Function Private Function ISoapTypeMapper_varType() As Long ISoapTypeMapper_varType = vbObject End Function Private Sub ISoapTypeMapper_write(ByVal pSoapSerializer As _ MSSOAPLib.ISoapSerializer, ByVal bstrEncoding As String, _ ByVal encodingMode As MSSOAPLib.enEncodingStyle, _ ByVal lFlags As Long, pvar As Variant) Dim si As New SongInfo Set si = pvar pSoapSerializer.startElement "SongTitle" mStringMapper.write pSoapSerializer, bstrEncoding, encodingMode, _ lFlags, si.SongTitle pSoapSerializer.endElement pSoapSerializer.startElement "AlbumTitle" mStringMapper.write pSoapSerializer, bstrEncoding, encodingMode, _ lFlags, si.AlbumTitle pSoapSerializer.endElement pSoapSerializer.startElement "Duration" mStringMapper.write pSoapSerializer, bstrEncoding, encodingMode, _ lFlags, si.Duration pSoapSerializer.endElement pSoapSerializer.startElement "MP3Info" mFileInfoMapper.ISoapTypeMapper_write pSoapSerializer, _ bstrEncoding, encodingMode, lFlags, si.MP3Info pSoapSerializer.endElement End Sub
<?xml version='1.0' ?> <servicemapping name='UltraMaxService'> <service name='UltraMaxService'> <using PROGID='UltraMaxMapper.FileInfoMapper' cachable='0' ID='UltraMaxFileInfoMapperObject' /> <using PROGID='UltraMaxMapper.SongInfoMapper' cachable='0' ID='UltraMaxSongInfoMapperObject' /> <types> <type name='FileInfo' targetNamespace='urn:UltraMaxService' uses='UltraMaxFileInfoMapperObject'/> <type name='SongInfo' targetNamespace='urn:UltraMaxService' uses='UltraMaxSongInfoMapperObject'/> </types> </service> </servicemapping>
Option Explicit Implements IHeaderHandler Public SessionID As String Private Const NAMESPACE As String = "urn:UltraMaxService" Private Const SESSIONID_NAME As String = "m_SessionID" Private Function IHeaderHandler_readHeader(ByVal pHeaderNode _ s MSXML2.IXMLDOMNode, ByVal pObject As Object) As Boolean If pHeaderNode.baseName = SESSIONID_NAME And _ pHeaderNode.namespaceURI = NAMESPACE Then ' Read session ID SessionID = pHeaderNode.Text IHeaderHandler_readHeader = True Else IHeaderHandler_readHeader = False End If End Function Private Function IHeaderHandler_willWriteHeaders() As Boolean IHeaderHandler_willWriteHeaders = True End Function Private Sub IHeaderHandler_writeHeaders(ByVal pSerializer As _ MSSOAPLib.ISoapSerializer, ByVal pObject As Object) ' Write session ID pSerializer.startHeaderElement SESSIONID_NAME, NAMESPACE pSerializer.writeString SessionID pSerializer.endHeaderElement End Sub