Using Web Services Instead of DCOM

 

Martin Wasznicky
Microsoft Corporation

February 2002

Summary: This document examines the advantages of using XML Web services over DCOM and demonstrates how to implement an XML Web service and consume it with a Microsoft .NET client application. (54 printed pages)

Objectives

  • Learn how to expose your business logic with XML Web services
  • Learn the differences between XML Web services and DCOM
  • Learn how to deploy XML Web services
  • Learn how to call XML Web services, synchronously and asynchronously

Assumptions

The following should be true for you to get the most out of this document:

  • You are familiar with coding in Microsoft® Visual Basic® 6.0
  • You have a basic understanding of Microsoft .NET
  • You have access to a Microsoft SQL Server™ database
  • You have access to Visual Basic 6.0, Microsoft ASP.NET, DCOM Server, Microsoft Visual Basic .NET and Microsoft Visual Studio® .NET

Contents

Introduction
DCOM versus Web Services
   DCOM Introduction
   XML Web Services Introduction
   ASP.NET Introduction
Converting DCOM to an XML Web Service
   The DCOM Server
   The DCOM Client
   The XML Web Service
   The .NET Windows Application Client
Summary
   About the Author

Introduction

The Microsoft .NET Framework provides a rich alternative to the Distributed COM (DCOM) protocol in the form of XML Web services provided by ASP.NET. XML Web services allow businesses to distribute their application services to a broader audience than is otherwise possible with DCOM. It accomplishes this by providing a loosely coupled messaging infrastructure, encapsulating and exposing business application logic using industry standard protocols and data formats such as Extensible Markup Language (XML), Hyper-Text Markup Language (HTTP) and SOAP (formerly Simple Object Access Protocol).

By incorporating these industry standards within XML Web services, businesses can expose their application logic to global clients and partners that were previously inaccessible. XML Web services provide a loosely coupled messaging infrastructure independent of vendor specific messaging implementations. Businesses can now easily integrate their existing applications with those residing on heterogeneous platforms, regardless of the programming model used. XML Web services allow businesses to deliver their application logic over the World Wide Web (WWW) to any type of client, on any platform, as long as they support the same industry standards set forth by, and submitted to the W3C.

This document examines the advantages of using XML Web services over DCOM and demonstrates how to implement an XML Web service and consume it with a .NET client application.

DCOM versus Web Services

DCOM enabled software developers to create applications that span multiple machine, network, and location boundaries, allowing scalability and ease-of-distribution across multiple tiers. Although additional complexity was introduced into the development effort, most of the benefits provided by DCOM (e.g., location independence, security and scalability) were realized to varying degrees. After the release of MTS and COM+, DCOM became easier to implement and became the standard protocol employed among most Microsoft solution providers. Later, Microsoft Application Center was released and provided load balancing and fault tolerance to COM+ components.

As the Internet evolves, the nature and scope of distributed applications must change to meet the underlying business needs. Businesses must integrate their applications with those that reside on heterogeneous platforms, and those that are built and deployed with varying programming models. Additionally, businesses need to communicate and expose their services to global clients and partners.

To address these needs, XML Web services were introduced as part of ASP.NET, which is part of the .NET Framework. Web services are based on open Internet standards, such as HTTP, XML, and SOAP. Using these open standards, Web services deliver application functionality across the Web to any type of client, on any platform.

Although XML Web services is the enabling technology, it is Visual Studio .NET that encapsulates its ease of use for developers. Visual Studio .NET provides a robust environment that allows the easy creation, deployment, and maintainability of applications developed using XML Web services.

DCOM Introduction

DCOM is based on the original Distributed Computing Environment (DCE) standard Remote Procedure Call (RPC) infrastructure and was created as an extension of Component Object Model (COM) to allow the creation of server objects on remote machines. In order for COM to create remote objects, the COM libraries need to know the network name of the server. Once the server name and CLSID (a globally unique identifier representing a COM Class within the server) are known, the Service Control Manager (SCM) on the client machine connects to the SCM on the server machine and requests creation of the remote machine's server object. Because DCOM is an extension of COM, it relies on the registry and COM libraries to supply the type library information of the object to create on the remote server machine. The remote server name is either configured in the registry or passed as an explicit parameter to a CoCreateInstanceEx call (in Visual Basic, this would be a CreateObject call).

Sub StartIE()
   Dim strProgID As String
   Dim oIE As InternetExplorer

   StrProgID = "InternetExplorer.Application.1"

   Set oIE=CreateObject(strProgID, "Defiant.waz.com")

End Sub

The DCOM configuration tool (Dcomcnfg.exe) is provided as an alternative way to set the remote machine name and security settings rather than editing the registry directly. Some configuration is usually done on the both the client and server machines. Security settings are configured on the server machine, whereas the remote machine name is configured on the client machine.

After the release of MTS, Microsoft Windows® 2000, and COM+, remote object activation became a little easier. Developers could install their components (in process and out of process) as configured server application components under COM+ or as server packages in MTS. COM+ and MTS provided the surrogate server process that allowed activation of in process components from remote clients. Both MTS and COM+ facilitate the export of a client proxy in the form of a setup program. Once exported, the proxy setup could be run on the client machines, installing all necessary type library registration and remote server name entries into the registry and the COM+ catalog. Once a remote object activation request is made, the SCM uses the type library information on the client to create a proxy object that would then be used to marshal invocation calls to its corresponding stub object on the remote server.

But DCOM wasn't perfect; it introduced new complexities. Like COM, whenever a server-side component is updated using DCOM, the type library information changes due to binary incompatibility. With DCOM, these changes need to be propagated to existing client machines. DCOM doesn't provide a mechanism for dynamically updating and binding to type library information; such information is stored in the registry, or with COM+ in the COM+ catalog. DCOM is a "chatty" protocol, pinging clients regularly to see if the clients are still alive. And because it doesn't support batch operations, it takes almost a dozen roundtrips to the remote server to complete a single method call. Using DCOM through firewalls becomes problematic because it dynamically allocates one port per process (configurable through the registry) and requires UPD and TCP ports 135-139 to be open. An alternative for enabling DCOM through firewalls exists by defining Tunneling TCP/IP as the underlying transport protocol. This allows DCOM to operate through some firewalls via port 80. But it's not very reliable, doesn't work through all firewalls, and introduces other limitations (lack of callback support, etc.). DCOM has certainly evolved over the years, in an effort to accommodate the demands of a changing environment. But because of its roots in older binary and component-based protocols, it still fails to deliver the flexibility needed in today's enterprise. DCOM is still inefficient, cumbersome to deploy and requires a fair amount of manual maintenance.

XML Web Services Introduction

XML Web services are based on open Web standards that are broadly supported and are used for communication and data formats. XML Web services provide the ability to expose application logic as URI-addressable resources, available to any client in a platform-independent way. COM-style type library information is no longer required on the client's machine and the Dcomcnfg.exe utility is no longer needed for distributed application configuration because Web services are self-describing. Any clients incorporating open Web standards for communication and data formatting (HTTP and XML) can query dynamically for Web service information and retrieve an XML document describing the location and interfaces supported by a particular XML Web service. These open standards make Web services indifferent to the operating system, object model, and programming language used. Web services are accessible to disparate systems, supporting application interoperability to an unprecedented level thanks to the ubiquity of HTTP and XML.

Instead of binary communication methods between applications, Web services use XML-encoded messages. Because XML-based messaging is used for the data interchange, a high level of abstraction exists between a Web service implementation and the client. This frees the client from needing to know anything about a Web service except for its location, method signatures, and return values. Additionally, most Web services are exposed and accessed via HTTP, virtually eliminating firewall issues.

Web services are not the ideal solution for all application models. Because it usually uses the HTTP transport and XML encoding, it's not as efficient or reliable as a binary protocol. On local Intranets, WANs, and LANs, .NET Remoting is a more appropriate solution.

For Web services to provide a level of interoperability, loosely coupled programming models, and communication, they depend on an infrastructure that provides the following standards-based protocols:

  • SOAP: The explicit messaging protocol used in Web service message exchanges. Although HTTP is used by XML Web services to provide the SOAP message transport protocol, it is not presumed. SMTP may be used with SOAP as well. XML is the format in which the message is serialized before being bound to the transport protocol.
  • WSDL: Web Service Description Language (WSDL 1.1) is the grammar describing the location and interfaces that a particular Web service supports. A Web service uses it to deliver an XML-formatted document to any requesting client. In the COM world, WSDL can be seen as synonymous to a type library. WSDL is considered the "contract" for a Web service.
  • DISCO: This is a Web Service Discovery mechanism. DISCO is the grammar used to describe the Uniform Resource Identifier (URI) of a Web service and contains references to the WSDL location. It usually resides at the root of a Web application and exists as an XML-formatted file.
  • UDDI: Universal Description Discovery and Integration is the directory for all Web services. This is a protocol that allows businesses to publish their developed Web services to a central directory so that they can be easily found and consumed by other business clients.
  • XML: Extensible Markup Language is a commonly used language for Internet-ready documents and development. Data is returned from a Web service in XML. If the Web service is invoked using SOAP, the parameters are also sent to the Web service method in XML.

A common scenario for a client that consumes application logic from a Web service might be (see Figure 1):

  1. The client queries a UDDI directory over HTTP for the location of a Web service.
  2. The client queries the Web service over HTTP for the location of the Web service's WSDL location via DISCO. This information is returned to the client in an XML-formatted message.
  3. The client retrieves the WSDL information for the Web service. This information is returned in an XML message using the WSDL grammar. The client uses the WSDL information to dynamically determine the interfaces and return types available from the Web service.
  4. The client makes XML/SOAP-encapsulated message calls to the Web service that conform to the WSDL information.

Figure 1. Web Service infrastructure example

ASP.NET Introduction

ASP.NET was designed to provide a Web services infrastructure and programming model that allows developers to create, deploy, and maintain Web services without needing to understand SOAP, WSDL, and DISCO. The goal was accomplished through the introduction of XML Web services, which is built on top of ASP.NET and the .NET Framework. Developers can easily create Web services by creating files with an ASMX extension (e.g., Customers.asmx) and deploying them as part of a Web application. Like .ASPX files, ASMX files are intercepted by an ISAPI extension (aspnet_isapi.dll) and the processing is done in a separate ASP.NET worker process. The ASMX file must either reference a .NET class or contain the class itself. The only mandatory entry in an ASMX file is the WebService directive specifying the class and the language.

<% WebService Language="vb" Class="Customers" %>

Optionally, the WebService directive can contain the location of the Customers class, if it was not created within the ASMX file, by declaring it with the Codebehind attribute. This attribute is just for Visual Studio .NET-developed XML Web services; otherwise the src attribute is used.

<% WebService Language="vb" Class="Customers" 
 Codebehind="Customers.vb" %>

In this case, the Customers class may optionally derive from the System.Web.Services.WebServices base class. Although not required, this derivation allows developers to access the ASP.NET intrinsics such as the Session, Context, Application, and User objects. This derivation allows the Web service to have the same state-management options as other ASP.NET applications. Note that the class must always import the System.Web.Services namespace.

WebMethod Attribute

Determining which methods in the Customers class are callable as part of the service is as simple as adding a custom attribute to the method implementation.

<WebMethod()>Public Sub Delete ( _
 ByVal customerID As String)

The WebMethod attribute allows XML Web services to determine at run time which methods should be exposed as part of the service. The WebMethod attribute also accepts a number of properties (listed in Table 1) that allow caching, enabling of session state, and even transaction support on a method-by-method level.

Table 1. Properties applied to the WebMethod attribute

Properties Description
BufferResponse Gets or sets whether the response for this request is buffered
CacheDuration Gets or sets the number of seconds the response should be held in the cache. The response is held in memory on the server for at least the time specified as the cache duration.
Description A message describing the Web service method. A listing in the WSDL file
EnableSession Indicates whether session state is enabled for a Web service method
MessageName The name used for the Web service method in the data passed to and returned from a Web service method. Useful for exposing overloaded functions
TransactionOption Indicates the transaction support of a Web service method

SOAP Headers

Although client requests can be made using HTTP-GET, HTTP-POST, or SOAP, SOAP provides the richest functionality of the wire formats. SOAP is a lightweight, message-based protocol that is built on XML (XSD version 2) and standard Internet protocols, such as HTTP and SMTP. The SOAP protocol specification consists of two main parts: one defines a mandatory envelope for encapsulating data; the other defines optional data encoding rules for representing application-defined data types.

The SOAP envelope defines a SOAP message that consists of a required Body element and an optional Header element (SOAP Headers). The Body element holds the information specific to the actual method call. SOAP messages are usually combined to implement a request/response design pattern.

SOAP Headers are an optional element within the SOAP envelope and usually contain data specific to a method call. They provide a unique way to send "out of band" data to the Web service. One example of this might be using SOAP Headers to marshal client credentials to the Web service for authentication. These are marshaled unencrypted unless SSL is used. A SOAP Header is created by defining a new class and deriving it from the System.Web.Services.Protocols.SoapHeader class. Then a method call can be preceded with the SoapHeader attribute, passing it a class-level variable that holds a pointer to the SOAP Header class.

< WebMethod(), _
 SoapHeaderAttribute("AuthHeaderMemberVariable")> _
 Public Function GetCustomer( _
 ByVal customerID As String) As DataSet

WSDL and Client Proxy Classes

The Wsdl.exe utility is included in the .NET Framework SDK and is used to generate a Web service client proxy class from the WSDL information of a Web service. Note: Wsdl.exe is not required if using Visual Studio .NET as an instance of the proxy class used to call methods on the remote XML Web service. The proxy class does all the work of marshalling the call over the wire to the specific Web service method. By default, the proxy class uses SOAP, however, it can support additional protocols such as HTTP-GET or HTTP-POST. Additionally, the proxy class exposes both synchronous and asynchronous methods for each method exposed by the Web service. Synchronous methods are represented by the name of the actual method call, and asynchronous methods are represented by the name of the method call preceded by Begin and End. The .NET Framework uses a common design pattern for all asynchronous calls. For example, for the Delete method call, there is a BeginDelete and an EndDelete used to call the Delete method asynchronously.

When the Begin method is called by a client, it starts the processing of the method call and returns control to the client immediately. When the Begin method call is made, the Delegate (address of the callback function) is passed to it along with any required arguments. The function that the Delegate represents is called by the Web service method when the results are returned to the client. When the client calls the End method (usually within the callback function passed to the Begin method), it returns the results of the Web service method. Here is an example of executing an asynchronous call.

' Class level variable holding the Customers proxy
' class representing the Web Service.
Private m_CustWebSrv As New LocalHost.Customers()

' Delete the current customer asynchronously
Public Sub DeleteCust(ByVal customerID As String)
   m_CustWebSrv.BeginDelete(customerID, _
   New AsyncCallback(AddressOf _
    Me.DeleteCustCallBack), 
    Nothing)
End Sub

' The function callback executed when the Web Service
' method completes processing
Public Sub DeleteCustCallBack(ByVal ar As IAsyncResult)
   ' Call the End method to return any errors or 
   ' resultsets
   m_CustWebSrv.EndDelete(ar)
End Sub

Converting DCOM to an XML Web Service

DCOM, Web services, and the ASP.NET implementation of Web services have been discussed to show the contrasts among them. Next, you'll examine some practical examples by creating a Visual Basic 6.0-based DCOM-enabled application (COM+) and porting it to an XML Web service.

First, a simple DCOM Server will be created, and then a Windows client application that accesses the server. Next the functionality will be replicated using XML Web services and Visual Basic .NET, accessed by a .NET Windows client. All the examples will access data maintained in the Northwind database that ships with Microsoft SQL Server.

The DCOM Server

Creating a Visual Basic 6.0 in-process server and installing it as a COM+-configured component with a Server Application activation type will implement the DCOM Server for this example.

Start by creating a new Microsoft Visual Basic ActiveX® DLL project and name it DemoVBServer. Make sure that the threading model is set to Apartment Threaded and the unattended execution option is checked.

Next, add a new class module to the project and name it Customers. Then add the following four public methods to the class:

Method Description
GetCustomer Optionally accepts the customer ID and returns either a specific customer record or all customer records in a disconnected ADODB.Recordset
Add Adds a new customer to the Northwind database
Delete Deletes only newly added customers from the Northwind database based on Customer ID
Update Updates the selected customer in the Northwind database based on Customer ID

Listed next is the source code for the Customers class. Modify the module level database connect string constant to point to a local SQL Server and add a reference to the Microsoft ActiveX Data Objects 2.5 (or higher).

' Define the module level connection info to SQL Server
Private Const m_CONNECTSTRING As String = "provider=sqloledb;user id=sa;" 
      & _ 
"password=;initial catalog=northwind;data source=localhost"

Public Function GetCustomer(Optional _
 ByVal CustomerID As String) As ADODB.Recordset

   Dim oConn       As ADODB.Connection
   Dim oRst        As ADODB.Recordset
   Dim strSQL      As String
   Const QT        As String = "'"

   ' Initialize the variables
   CustomerID = Trim$(CustomerID)
   Set oConn = New ADODB.Connection
   Set oRst = New ADODB.Recordset

   ' Determine sql
   If Len(CustomerID) < 1 Then
      strSQL = "SELECT * FROM Customers"
   Else
      strSQL = "SELECT * FROM Customers " & _
       "WHERE CustomerID=" & QT & CustomerID & QT
   End If

   ' establish the connection and 
   ' return disconnected recordset
   oConn.Open m_CONNECTSTRING
   oConn.CursorLocation = adUseClient
   oRst.Open strSQL, oConn, _
    adOpenForwardOnly, adLockReadOnly
   oRst.ActiveConnection = Nothing
   oConn.Close
   Set oConn = Nothing

   ' Return the recordset
   Set GetCustomer = oRst

   ' Clean up
   Set oRst = Nothing
End Function

Public Sub Add(ByVal CustomerID As String, _
   ByVal CompanyName As String, _
   ByVal ContactName As String, _
   ByVal ContactTitle As String, _
   ByVal Address As String, _
   ByVal City As String, ByVal Region As String, _
   ByVal PostalCode As String, ByVal Country As String, _
   ByVal Phone As String, ByVal Fax As String)

   Dim oConn       As ADODB.Connection
   Dim strSQL      As String
   Const QT        As String = "'"

   ' Validate
   If Len(Trim$(CustomerID)) < 1 Then _
      Err.Raise 9999, "Add", _
      "You must enter a Customer ID to add."

   ' Initialize the variables
   CustomerID = QT & UCase(Trim$(CustomerID)) & QT
   CompanyName = QT & Trim$(CompanyName) & QT
   ContactName = QT & Trim$(ContactName) & QT
   ContactTitle = QT & Trim$(ContactTitle) & QT
   Address = QT & Trim$(Address) & QT
   City = QT & Trim$(City) & QT
   Region = QT & Trim$(Region) & QT
   PostalCode = QT & Trim$(PostalCode) & QT
   Country = QT & Trim$(Country) & QT
   Phone = QT & Trim$(Phone) & QT
   Fax = QT & Trim$(Fax) & QT
   ' Initialize the sql string
   strSQL = "INSERT INTO Customers " & _ 
    (CustomerID,CompanyName,ContactName," & _
    "ContactTitle,Address,City,Region,PostalCode," & _
    "Country,Phone,Fax) " & _
    "VALUES (" & CustomerID & "," & CompanyName & _
    "," & ContactName & "," & ContactTitle & _
    "," & Address & "," & City & "," & Region & _
    "," & PostalCode & "," & Country & "," & _
    Phone & "," & Fax & ")"

   ' Create the connection object and open the connection
   Set oConn = New ADODB.Connection
   oConn.ConnectionString = m_CONNECTSTRING
   oConn.Open

   ' Add the customer to the table
   oConn.Execute strSQL

   ' Clean up
   oConn.Close
   Set oConn = Nothing
End Sub

Public Sub Update(ByVal CustomerID As String, _
   ByVal CompanyName As String, _
   ByVal ContactName As String, _
   ByVal ContactTitle As String, _
   ByVal Address As String, ByVal City As String, _
   ByVal Region As String, ByVal PostalCode As String, _
   ByVal Country As String, ByVal Phone As String, _
   ByVal Fax As String)

   Dim oConn       As ADODB.Connection
   Dim strSQL      As String
   Const QT        As String = "'"

   ' Validate
   If Len(Trim$(CustomerID)) < 1 Then _
      Err.Raise 9999, "Update", _
       "You must select a Customer ID to update."

   ' Initialize the variables
   CustomerID = QT & Trim$(CustomerID) & QT
   CompanyName = QT & Trim$(CompanyName) & QT
   ContactName = QT & Trim$(ContactName) & QT
   ContactTitle = QT & Trim$(ContactTitle) & QT
   Address = QT & Trim$(Address) & QT
   City = QT & Trim$(City) & QT
   Region = QT & Trim$(Region) & QT
   PostalCode = QT & Trim$(PostalCode) & QT
   Country = QT & Trim$(Country) & QT
   Phone = QT & Trim$(Phone) & QT
   Fax = QT & Trim$(Fax) & QT

   ' Initialize the sql string
   strSQL = "UPDATE Customers SET " & _
    "CompanyName=" & CompanyName & _
    ",ContactName=" & ContactName & _
    ",ContactTitle=" & ContactTitle & _
    ",Address=" & Address & _
    ",City=" & City & _
    ",Region=" & Region & _
    ",PostalCode=" & PostalCode & _
    ",Country=" & Country & _
    ",Phone=" & Phone & _
    ",Fax=" & Fax & _
    "WHERE CustomerID=" & CustomerID

   ' Create the connection object and open the connection
   Set oConn = New ADODB.Connection
   oConn.ConnectionString = m_CONNECTSTRING
   oConn.Open

   ' Add the customer to the table
   oConn.Execute strSQL

   ' Clean up
   oConn.Close
   Set oConn = Nothing
End Sub

Sub Delete(ByVal CustomerID As String)

   Dim oConn       As ADODB.Connection
   Dim strSQL      As String
   Const QT        As String = "'"

   ' Validate
   If Len(Trim$(CustomerID)) < 1 Then _
      Err.Raise 9999, "Delete", _
       "You must select a Customer ID to delete."

   ' Initialize the variables
   CustomerID = QT & Trim$(CustomerID) & QT

   ' Initialize the sql string
   strSQL = "DELETE FROM Customers " & _
    "WHERE CustomerID= " & CustomerID

   ' Create the connection object and open the connection
   Set oConn = New ADODB.Connection
   oConn.ConnectionString = m_CONNECTSTRING
   oConn.Open

   ' Add the customer to the table
   oConn.Execute strSQL

   ' Clean up
   oConn.Close
   Set oConn = Nothing
End Sub

Next, compile the project into the DemoVBServer.DLL, open the COM+ Explorer (see Figure 2), and create a new COM+ Server Application called DemoVBServer set to run under a specific security context (user id). Then add the DemoVBServer.Customers class as a new component within the DemoVBServer COM+ Server Application.

Figure 2. DemoVBServer COM+ Server Application and Customer's component in COM+ Explorer

This completes the creation of the DCOM server. Next, the client proxy setup must be exported from the COM+ Explorer. The proxy setup will need to be executed on every client machine that accesses the server. To create the proxy setup, execute the COM+ Export function. Name the client proxy setup DemoVBServer.MSI, to be a Microsoft Windows Installer file.

The DCOM Client

Now that the DCOM Server is complete, create a Visual Basic 6.0 Windows Application client to access it. Start by creating a new Standard EXE Project and naming it DemoVBClient.

Next, rename the existing form to frmMain and define it as the default startup object. Add controls to the form so that it looks like the form in Figure 3.

Figure 3. The DemoVBClient Standard EXE

The table below contains the names and types of the controls required on frmMain.

Control Type
cmdClear Command Button
cmdUpdate Command Button
cmdAdd Command Button
cmdDelete Command Button
txtCustomerID Text Box
txtCompany Text Box
txtContactName Text Box
txtContactTitle Text Box
txtAddress Text Box
txtCity Text Box
txtRegion Text Box
txtPostalCode Text Box
txtCountry Text Box
lstCust List Box

Listed below is the source code for the frmMain form. Make sure to add a reference to the Microsoft ActiveX Data Objects 2.5 (or higher) and the DemoVBServer type libraries.

' Module level reference to DCOM Server
Private moCust As Customers 

Private Sub cmdAdd_Click()
On Error GoTo Add_Err

   Dim iCnt    As Integer

   ' Add the customer
   moCust.Add txtCustomerID.Text, txtCompany.Text, _
      txtContactName.Text, txtContactTitle.Text, _
      txtAddress.Text, txtCity.Text, txtRegion.Text, _
      txtPostalCode.Text, txtCountry.Text, _
      vbNullString, vbNullString

   ' Refill the list
   FillCustList

   ' Search for the just added customer and select it
   For iCnt = 0 To lstCust.ListCount
      If StrComp(Trim$(lstCust.List(iCnt)), _
         Trim$(txtCustomerID.Text), _
            vbTextCompare) = 0 Then
         lstCust.Selected(iCnt) = True
      End If
   Next
   Exit Sub
Add_Err:
   MsgBox Err.Description
End Sub

Private Sub cmdClear_Click()
   ' Clear the form
   ClearCustInfo
End Sub

Private Sub cmdDelete_Click()
On Error GoTo Delete_Err

   ' Delete the current customer
   moCust.Delete lstCust.List(lstCust.ListIndex)

   ' Clear the form
   ClearCustInfo

   ' Refill the list
   FillCustList
   Exit Sub

Delete_Err:
   MsgBox Err.Description
End Sub

Private Sub cmdUpdate_Click()
On Error GoTo Update_Err

   ' Update the customer
   moCust.Update lstCust.List(lstCust.ListIndex), _
      txtCompany.Text, _
      txtContactName.Text, txtContactTitle.Text, _
      txtAddress.Text, txtCity.Text, txtRegion.Text, _
      txtPostalCode.Text, txtCountry.Text, _
      vbNullString, vbNullString

   Exit Sub
Update_Err:
   MsgBox Err.Description
End Sub

Private Sub Form_Load()
On Error GoTo Load_Err

   ' Initialize variables
   Set moCust = New Customers

   ' Populate the list box
   FillCustList
   Exit Sub
Load_Err:
   MsgBox Err.Description
   Set moCust=Nothing
End Sub

Private Sub Form_Unload(Cancel As Integer)
On Error Resume Next

   ' Cleanup
   Set moCust = Nothing
End Sub

Private Sub lstCust_Click()
On Error GoTo lstCustClick_Err

   Dim oRst                As ADODB.Recordset
   Dim strCustomerID       As String

   strCustomerID = lstCust.List(lstCust.ListIndex)

   If Len(strCustomerID) > 0 Then

      ' Retrieve the info
      Set oRst = moCust.GetCustomer(strCustomerID)
      If oRst.EOF And oRst.BOF Then _
         Err.Raise 8888, "listclick", _
          "A customer record could not be found for " & _
          strCustomerID & "."

      ' Populate the form
      FillCustomerInfo oRst
   End If

lstCustClick_Exit:
   ' Clean up
   Set oRst = Nothing
   Exit Sub

lstCustClick_Err:
   MsgBox Err.Description
   Resume lstCustClick_Exit
End Sub

Private Sub FillCustList()

   Dim oRst As ADODB.Recordset

   ' Get the list of customers
   Set oRst = moCust.GetCustomer()

   ' Clear the list box and fill it
   lstCust.Clear
   Do Until oRst.EOF
      lstCust.AddItem oRst.Collect(0)
      oRst.MoveNext
   Loop

   Set oRst = Nothing
End Sub
Private Sub FillCustomerInfo(ByRef oCust As _
 ADODB.Recordset)
On Error Resume Next

   ' Fill in the form
   txtCustomerID.Text = oCust.Collect(0) & vbNullString
   txtCompany.Text = oCust.Collect(1) & vbNullString
   txtContactName.Text = oCust.Collect(2) & vbNullString
   txtContactTitle.Text = oCust.Collect(3) & vbNullString
   txtAddress.Text = oCust.Collect(4) & vbNullString
   txtCity.Text = oCust.Collect(5) & vbNullString
   txtRegion.Text = oCust.Collect(6) & vbNullString
   txtPostalCode.Text = oCust.Collect(7) & vbNullString
   txtCountry.Text = oCust.Collect(8) & vbNullString
End Sub

Private Sub ClearCustInfo()
On Error Resume Next

   Dim ctl         As Control

   ' Clear all the text boxes.
   For Each ctl In Me.Controls
      If TypeOf ctl Is TextBox Then 
         ctl.Text = vbNullString
      End If
   Next

   Set ctl = Nothing
End Sub

Compile the project into the DemoVBClient.exe to finish the client application. Before deploying the application, execute the DemoVBServer.msi proxy setup on the target client machine. Copy the DemoVBClient.exe to the target client machine. The application should start up without exception.

The XML Web Service

Now comes the fun part! Start by recreating the previous DCOM Server as an XML Web service using Visual Basic .NET and Visual Studio .NET. But first, add some of the functionality that XML Web services offers, like SOAP Headers. That functionality can be incorporated into the new XML Web service to demonstrate a custom authentication scheme using SOAP Headers.

The Web Service

For this section, create a new ASP.NET Web service project using Visual Basic .NET and name it DemoWebSrv. Specify that the project will be created under https://localhost/DemoWebSrv. Visual Studio .NET will create an IIS virtual directory pointing to the physical location. The virtual directory URL will be https://localhost/DemoWebSrv. The files that are automatically generated for the project can be viewed in the solution explorer and should look like Figure 4 (click Show All Files).

Figure 4. Web services project created in Solution Explorer with Show All Files selected

Open the project Properties dialog box and set the following properties:

  • Option Explicit = ON
  • Option Strict = On
  • Option Compare = Text
  • Configuration = Release

Next, rename Service1.asmx file to Customers.asmx. This file represents the final Web service that will be available. When the file is renamed, Visual Studio .NET also renames the source code file for the service to Customers.asmx.vb and updates the Customer.asmx file with the correct references. Right-click this file and select View Code.

At the top of the Customers.asmx.vb file add the following:

Option Compare Text
Option Explicit ON
Option Strict On

Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.Security.Principal

The Imports statement allows access to objects in the .NET Framework hierarchy without fully qualifying their namespace. For example, instead of specifying

Dim custDataSet as System.Data.DataSet

Use the following declaration instead:

Dim custDataSet as DataSet

The next series of steps involve the creation of the SOAP Header class and the Customers class. The Customers class will contain all the exposed methods of the Web service. Both classes will be created within the Customers.asmx.vb file. The first class to create is a SOAP Header class called AuthHeader. This is used to accept a username, password, and an IsAuthenticated parameter from the client. These are later optionally validated within the GetCustomer method of the Customers class using the ValidateUser method. Here is the source code for this class.

Public Class AuthHeader
   Inherits SoapHeader

   ' These hold the values retrieved from the SOAP Header 
   ' sent by the client.
   Private UsernameEx As String = Nothing
   Private PasswordEx As String = Nothing
   Private Authenticated As Boolean = False

   Public Sub New()
      MyBase.new()
   End Sub

   Public Property Username() As String
      Get
         Username = UsernameEx
      End Get
      Set(ByVal Value As String)
         UsernameEx = Value
      End Set
   End Property

   Public Property Password() As String
      Get
         Password = PasswordEx
      End Get
      Set(ByVal Value As String)
         PasswordEx = Value
      End Set
   End Property

   Public Property IsAuthenticated() As Boolean
      Get
         IsAuthenticated = Authenticated
      End Get
      Set(ByVal Value As Boolean)
         Authenticated = Value
      End Set
   End Property

   Public Sub ValidateUser( _
    Optional ByVal Role As String = Nothing)

   ' This method is basically used to 
   ' validate the user name sent in the Soap
   ' Header for custom authentication
   ' implementation. If not validated, then the
   ' method throws a SoapHeaderException exception.

      If Not IsAuthenticated Then _
         Throw New SoapHeaderException( _
          "Only authenticated users can access " & _
          "the Web Service.", _
          SoapException.ClientFaultCode)

      ' Validate user and role.
      ' We can use whatever mechanism we want 
      ' here to validate a user
      Select Case UCase(UsernameEx)
         Case UCase("Administrator")
            If Role Is Nothing Then Return
         Case Else
            ' This will allow anyone to access. 
            ' However, here would be 
            ' a good place to look up the user in 
            ' a database or in the
            ' active directory.
            If Role Is Nothing Then Return
      End Select
   End Sub
End Class

Now, create the main Web service class by renaming the existing Service1 class to Customers. This class is already set to derive from System.Web.Services.WebService, which allows access to Application and Session state ASP.NET intrinsics.

Notice the auto-generated code that was produced in an area marked by the #Region directive (see listing below). This area should be left alone because it contains auto-generated methods required to initialize the Web service class so that it can support optional components in the Web services Designer exposed by Visual Studio .NET.

#Region " Web Services Designer Generated Code "
   Public Sub New()
      MyBase.New()

      ' This call is required by the Web Services Designer.
      InitializeComponent()
   End Sub

   ' Required by the Web Services Designer
   Private components As System.ComponentModel.Container

   ' NOTE: The following procedure is required by 
   ' the Web Services Designer
   ' It can be modified using the Web Services Designer.  
   ' Do not modify it using the code editor.
   <System.Diagnostics.DebuggerStepThroughAttribute()> _
   Private Sub InitializeComponent()
      components = New System.ComponentModel.Container()
   End Sub

   Protected Overloads Overrides Sub Dispose(ByVal _
    disposing As Boolean)
   End Sub
#End Region

Following the new class declaration but preceding the #Region directive, add a public class level variable and constant to hold an instance of the AuthHeader class and the database connect string to the Northwind SQL Server database.

Public AuthHeaderMemberVariable As AuthHeader

' This is the connect string for all database access.
Private Const DbConnString As String = _
 "data source=defiant;" & _
 "initial catalog=Northwind; " & _
 "persist security info=False;" & _ "user id=sa;"

Next, add the remaining methods to the Customer class directly beneath the #End Region directive. #Region directives allow users to specify blocks of code that can expand or collapse within the Visual Studio .NET IDE. These methods include all the methods found in the previous DCOM server example, such as Add, Delete, Update, and GetCustomer (see the table below).

Method Description
GetCustomer Optionally accepts the customer ID and returns either a specific customer record or all customer records in a System.Data.DataSet
Add Adds a new customer to the Northwind database
Delete Deletes only newly added customers from the Northwind database based on Customer ID
Update Updates the selected customer in the Northwind database based on Customer ID

Here is the source code for the remaining functions.

<WebMethod(Description:="Adds a new customer " & _
     "to the customer database", EnableSession:=False)> _
     Public Sub Add(ByVal customerID As String, _
     ByVal companyName As String, _
     ByVal contactName As String, _
     ByVal contactTitle As String, _
     ByVal address As String, _
     ByVal city As String, ByVal region As String, _
     ByVal postalCode As String, _
     ByVal country As String, _
     ByVal phone As String, ByVal fax As String)

        Dim SqlConnection As New _
         SqlConnection(DbConnString)

        ' Initialize variables
        customerID = UCase(Trim(customerID))
        companyName = Trim(companyName)
        contactName = Trim(contactName)
        contactTitle = Trim(contactTitle)
        address = Trim(address)
        city = Trim(city)
        region = Trim(region)
        postalCode = Trim(postalCode)
        country = Trim(country)
        phone = Trim(phone)
        fax = Trim(fax)

        ' This block creates the insert command and 
        ' executes it.
        Try
            ' validate
            If Len(customerID) < 1 Then _
               Throw New ArgumentException("You must " & _
                 "enter a Customer ID " & _
                 "to Add customer info.", "customerID")

            ' Open the connection object
            SqlConnection.Open()

            ' Create the command object
            Dim AddCmd As New SqlCommand("INSERT " & _
             "INTO Customers(CustomerID," & _
             "CompanyName, ContactName, ContactTitle," & _
             "Address, City, Region, PostalCode, " & _
             "Country, Phone, Fax) VALUES " & _
             "(@CustomerID, @CompanyName, " & _
             "@ContactName, @ContactTitle, " & _
             "@Address, @City, @Region, " & _
             "@PostalCode, @Country" & _
             ", @Phone, @Fax)", SqlConnection)

            ' Create the parameter and set it
            With AddCmd.Parameters
                .Add(New SqlParameter("@CustomerID", _
                   SqlDbType.NChar, _
                   Len(customerID), _
                   ParameterDirection.Input, True, _
                   CType(0, Byte), CType(0, Byte), _
                   "CustomerID", _
                   DataRowVersion.Proposed, customerID))
                .Add(New SqlParameter("@CompanyName", _
                   SqlDbType.NChar, _
                   0, ParameterDirection.Input, _
                   False, CType(0, Byte),CType(0, Byte), _
                   "CompanyName", _
                   DataRowVersion.Proposed, companyName))
                .Add(New SqlParameter("@ContactName", _
                   SqlDbType.NChar, _
                   0, ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "ContactName", _
                   DataRowVersion.Proposed, contactName))
                .Add(New SqlParameter("@ContactTitle", _
                   SqlDbType.NChar, _
                   0, ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "ContactTitle", _
                   DataRowVersion.Proposed, contactTitle))
                .Add(New SqlParameter("@Address", _
                   SqlDbType.NChar, _
                   0, ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "Address", DataRowVersion.Proposed, _
                   address))
                .Add(New SqlParameter("@City", _
                   SqlDbType.NChar, _
                   0, ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "City", DataRowVersion.Proposed, city))
                .Add(New SqlParameter("@Region", _
                   SqlDbType.NChar, _
                   0, ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "Region", DataRowVersion.Proposed, _
                   region))
                .Add(New SqlParameter("@PostalCode", _
                   SqlDbType.NChar, _
                   0, ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "PostalCode", _
                   DataRowVersion.Proposed, postalCode))
                .Add(New SqlParameter("@Country", _
                   SqlDbType.NChar, _
                   0, ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "Country", DataRowVersion.Proposed, _
                   country))
                .Add(New SqlParameter("@Phone", _
                   SqlDbType.NChar, _
                   0, ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "Phone", DataRowVersion.Proposed, _
                   phone))
                .Add(New SqlParameter("@Fax", _
                   SqlDbType.NChar, _
                   0, ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "Fax", DataRowVersion.Proposed, fax))
            End With

            ' Execute the non row returning parameter
            AddCmd.ExecuteNonQuery()

        Catch SqlExc As SqlException
            Throw New Exception("SQL Library Error. " & _
             "Unable to add the customer " & _
             "the customer database.", SqlExc)
        Catch ArgExc As ArgumentException
            Throw ArgExc
        Catch Exc As Exception
            Throw New Exception("Unexpected error " & _
             "occurred adding user to " & _
             "customer database", Exc)
        Finally
            ' Close the connection
            If SqlConnection.State <> _
             ConnectionState.Closed Then _
             SqlConnection.Close()
        End Try
   End Sub

   <WebMethod(Description:="Updates a customer " & _
     "by customer ID in the customer database", _
     EnableSession:=False)> _
     Public Sub Update(ByVal customerID As String, _
     ByVal companyName As String, _
     ByVal contactName As String, _
     ByVal contactTitle As String, _
     ByVal address As String, _
     ByVal city As String, ByVal region As String, _
     ByVal postalCode As String, _
     ByVal country As String, _
     ByVal phone As String, ByVal fax As String)

        Dim SqlConnection As New _
         SqlConnection(DbConnString)

        ' Initialize variables
        customerID = UCase(Trim(customerID))
        companyName = Trim(companyName)
        contactName = Trim(contactName)
        contactTitle = Trim(contactTitle)
        address = Trim(address)
        city = Trim(city)
        region = Trim(region)
        postalCode = Trim(postalCode)
        country = Trim(country)
        phone = Trim(phone)
        fax = Trim(fax)

        ' This block creates the update command and
        ' executes it.
        Try
            ' validate
            If Len(customerID) < 1 Then _
               Throw New ArgumentException("You must " & _
                 "select a customer " & _
                 "ID to Update.", "customerID")

            ' Open the connection object
            SqlConnection.Open()

            ' Create the command object
            Dim UpdateCmd As New SqlCommand("UPDATE " & _
             "Customers SET CompanyName=@CompanyName," & _
             "ContactName = @ContactName," & _
             "ContactTitle = @ContactTitle, " & _
             "Address = @Address, City=@City, Region=" & _
             "@Region, PostalCode = @PostalCode, " & _
             "Country=@Country, Phone=@Phone, Fax = " & _
             "@Fax WHERE (CustomerID = @CustomerID)", _
             SqlConnection)

            ' Create the parameter and set it
            With UpdateCmd.Parameters
                .Add(New SqlParameter("@CompanyName", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   False, CType(0, Byte),CType(0, Byte), _
                   "CompanyName",DataRowVersion.Current, _
                   companyName))
                .Add(New SqlParameter("@ContactName", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "ContactName", _
                   DataRowVersion.Current, contactName))
                .Add(New SqlParameter("@ContactTitle", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "ContactTitle", _
                   DataRowVersion.Current, contactTitle))
                .Add(New SqlParameter("@Address", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "Address", DataRowVersion.Current, _
                   address))
                .Add(New SqlParameter("@City", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "City", DataRowVersion.Current, city))
                .Add(New SqlParameter("@Region", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "Region", DataRowVersion.Current, _
                   region))
                .Add(New SqlParameter("@PostalCode", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "PostalCode", DataRowVersion.Current, _
                    postalCode))
                .Add(New SqlParameter("@Country", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "Country", DataRowVersion.Current, _
                   country))
                .Add(New SqlParameter("@Phone", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "Phone", DataRowVersion.Current, _
                   phone))
                .Add(New SqlParameter("@Fax", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "Fax", DataRowVersion.Current, fax))
                .Add(New SqlParameter("@CustomerID", _
                   SqlDbType.NChar, 0, _
                   ParameterDirection.Input, _
                   True, CType(0, Byte), CType(0, Byte), _
                   "CustomerID", DataRowVersion.Current, _
                    customerID))
            End With

            ' Execute the non row returning parameter
            UpdateCmd.ExecuteNonQuery()

        Catch SqlExc As SqlException
            Throw New Exception("SQL Library Error. " & _
             "Unable to update the " & _
             "customer in the customer database.", SqlExc)
        Catch ArgExc As ArgumentException
            Throw ArgExc
        Catch Exc As Exception
            Throw New Exception("Unexpected error " & _
             "occurred updating user " & _
             "in customer database", Exc)
        Finally
            ' Close the connection
            If SqlConnection.State <> _
             ConnectionState.Closed Then _
             SqlConnection.Close()
        End Try
   End Sub

   <WebMethod(Description:="Deletes a customer " & _
     "by customer ID from " & _
     "the customer database", EnableSession:=False)> _
     Public Sub Delete(ByVal customerID As String)

        Dim SqlConnection As New _
         SqlConnection(DbConnString)

        ' This block creates the delete command and
        ' executes it.
        Try
            ' validate
            If Len(customerID) < 1 Then _
               Throw New ArgumentException("You must " & _
                 "select a customer ID " & _
                 "to delete.", "customerID")
            ' Open the connection object
            SqlConnection.Open()

            ' Create the command object
            Dim DeleteCmd As New SqlCommand("DELETE " & _
             "FROM Customers WHERE (CustomerID =" & _
             "@CustomerID)", SqlConnection)

            ' Create the parameter and set it
            DeleteCmd.Parameters.Add(New _
             SqlParameter("@CustomerID", _
            SqlDbType.NChar, 0, _
            ParameterDirection.Input, True, _
            CType(0, Byte), CType(0, Byte), _
            "CustomerID", DataRowVersion.Current, _
            customerID))

            ' Execute the non row returning parameter
            DeleteCmd.ExecuteNonQuery()

        Catch SqlExc As SqlException
            Throw New Exception("SQL Library Error. " & _
             "Unable to delete the " & _
             "customer from the customer database.", _
             SqlExc)
        Catch ArgExc As ArgumentException
            Throw ArgExc
        Catch Exc As Exception
            Throw New Exception("Unexpected error " & _
             "occurred deleting user " & _
             "from customer database", Exc)
        Finally
            ' Close the connection
            If SqlConnection.State <> _
                ConnectionState.Closed Then _
                SqlConnection.Close()
        End Try
   End Sub

   <WebMethod(EnableSession:=False, _
        Description:="Returns all customers " & _
        "or a customer by customer ID"), _
        SoapHeaderAttribute("AuthHeaderMemberVariable", _
        Direction:=SoapHeaderDirection.In, _
        Required:=True)> _
        Public Function GetCustomer( _
        ByVal customerID As String) As DataSet

        Dim CustDataSet As New DataSet()
        Dim SqlConnection As New _
         SqlConnection(DbConnString)
        Dim SqlText As String = Nothing

        ' Query the database and return the results
        Try
            ' Validate that the user has access
            AuthHeaderMemberVariable.ValidateUser()

            ' Initialize variables
            customerID = Trim(customerID)

            ' Create the correct sql
            If Len(customerID) > 0 Then
                SqlText = "select * from Customers " & _
                 "where CustomerId='" & customerID & "'"
            Else
                SqlText = "select * from Customers"
            End If

            ' Open the connection object
            SqlConnection.Open()

            ' Create the adapter and fill dataset 
            ' With customer(s)
            Dim CustAdapter As New _
             SqlDataAdapter(SqlText, SqlConnection)
            CustAdapter.Fill(CustDataSet)

            ' Return the customer ds
            Return CustDataSet

        Catch SoapExc As SoapHeaderException
            Throw SoapExc
        Catch SqlExc As SqlException
            Throw New Exception("SQL Library Error. " & _
             "Unable to retrieve " & _
            "customer info from the customer database.", _
            SqlExc)
        Catch Exc As Exception
            Throw New Exception("Unable to retrieve " & _
             "customer info from the " & _
             "customer database.", Exc)
        Finally
            ' Close the connection
            If SqlConnection.State <> _
                ConnectionState.Closed Then _
                SqlConnection.Close()
        End Try
   End Function

There is one method call signature, GetCustomer, that helps understand the functionality exposed by the Web service. Here is the method signature of GetCustomer.

<WebMethod(EnableSession:=False, _
 Description:= _
 "Returns all customers or a customer by  customer ID"), _
 SoapHeader("AuthHeaderMemberVariable", _
 Direction:=SoapHeaderDirection.In, Required:=True)> _
 Public Function GetCustomer( _
 ByVal customerID As String) As DataSet

The only difference between this signature and the one in the previous DCOM server example is that this one is decorated with Attributes. Attributes applied to a method signature are enclosed in "<" and ">" when using Visual Basic .NET, and enclosed in "[" and "]" when using C#. At design time, these describe the functionality that will be associated with the component. Because they represent classes themselves, the component inherits the base class of the Attribute.

The first Attribute, WebMethod represents the WebMethodAttribute class and identifies the method as callable from a Web service. It is also used to enable the session state and append a description to the method. This description will be represented in the WSDL information produced for the Web service. This can be viewed by navigating to a Web service and appending ?WSDL to the URL like so, http://MachineName/WebServiceVirtualDirectory/ServiceName.asmx?WSDL. Finally, the SoapHeader Attribute represents the SoapHeaderAttribute class. This Attribute passes the public variable instance of the AuthHeader class. This variable will be populated with the SOAP Header information sent by the client and become available within the method call.

Once the code is completed in the project, execute the build process to create the DemoWebSrv.dll. After the build is complete, view the service description page by opening https://localhost/DemoWebSrv/Customers.asmx in a browser (see Figure 5). This page is auto-generated by ASP.NET and will include links to each method call defined by the WebMethod Attribute. Each link is directed to a page that allows the invocation of the method using the HTTP-GET protocol. Additionally, the Service Description link displays the WSDL service description information for the Web service. This can later be used with the WSDL.EXE tool to generate client proxy classes.

Figure 5. Service Description page auto-generated by ASP.NET

The .NET Windows Application Client

Now that a Web service has been created to support the previous DCOM functionality, create a Visual Basic .NET Windows Application client to access it. Start a new Visual Basic .NET Windows Application project and name it DemoWebClient.

Open the project properties dialog box and set the following properties:

  • Option Explicit = ON
  • Option Strict = On
  • Option Compare = Text
  • Configuration = Release

Next, rename Form1.vb form to frmMain.vb. This represents the start-up form for the application. Right-click the form and select View Code. At the top of the frmMain.vb file, add the following:

Option Compare Text
Option Explicit ON
Option Strict On

Imports System
Imports System.Security.Principal
Imports System.Windows.Forms
Imports System.Web.Services.Protocols

Add controls to the form so that it looks like the form in Figure 6.

Figure 6. frmMain in DemoWebClient project

The table below contains the names and types of the controls required on frmMain.

Control Type
cmdCancel Button
cmdClear Button
cmdUpdate Button
cmdAdd Button
cmdDelete Button
txtCustomerID Text Box
txtCompany Text Box
txtContactName Text Box
txtContactTitle Text Box
txtAddress Text Box
txtCity Text Box
txtRegion Text Box
txtPostalCode Text Box
txtCountry Text Box
lstCust List Box

The next step is to add a reference to the previously created Web service in the client application project. This is done on the Project menu by clikcking Add Web Reference… from the menu bar. This will display the Add Web Reference dialog box (see Figure 7). In the Address box, type in the URL and filename of the Web service to reference (https://localhost/DemoWebSrv/Customers.asmx) and press Enter. This will bring up the Web service description page in the left-hand pane and enable the Add Reference button. Click Add Reference to add the reference to the project.

Figure 7. Add Web Reference dialog box

Once the Web reference is added to the project, a couple of interesting things happen. First, a new subfolder with the same name as the server hosting the Web server (in this case localhost) is created under the Web References folder of the project. Within that folder, several files are created.

  • Reference.map: Used for mapping Web service references with the local files created, specifically the .disco and .wsdl files.
  • Customers.disco: Contains the SOAP Discovery information for the newly added Web service reference
  • Customers.wsdl: The WSDL file generated by the Web service that specifically describes its consumable interfaces.
  • Customers.vb: A Visual Basic .NET proxy class that encapsulates all the Web service calls, providing strong typing on the client side. It is generated automatically by Visual Studio .NET using the Wsdl.exe tool and contains all the synchronous and asynchronous method calls exposed by the Web service.

The automatic generation and inclusion of these files by Visual Studio .NET makes the integration of a Web service appear seamless by hiding the complexity of the Web service messaging from the developer.

Finish the client application by adding the source code necessary for interacting with the Web service. In the code behind frmMain, declare the following variables, following the Form1 class declaration but preceding the #Region directive.

' This represents the Web Service
Private m_CustWebSrv As New localhost.Customers()   
' This holds the current user.
Private m_CurrentUser As String = Nothing           
' This is the soap header for the Web Service
Private m_AuthHeader As New localhost.AuthHeader()  

The variable m_CustWebSrv is early bound to an instance of the client side Customers proxy class. This makes method calls to the Web service as easy as Object.Method(). Additionally, the SOAP Header class (AuthHeader) is also defined in the Customers.vb file, providing for strong typing as well.

Next, add the remaining methods to the Form1 class directly beneath the #End Region directive. Start by adding the code that will execute when the form opens (listed below). This is accomplished by sinking the method with the Mybase.Load event using the Handles keyword. The method retrieves the current user name, sets the AuthHeader class variable and calls the FillCustList method to populate the list box with Customer IDs

Private Sub FormLoadEventHandler( _
 ByVal eventSender As System.Object, _
 ByVal eventArgs As System.EventArgs) _
 Handles MyBase.Load

   ' Populate the list box and retrieve the current user
   Try
      ' set the cursor to the hourglass
      Me.Cursor = Cursors.WaitCursor

      ' Get current user
      Dim wp As New _
       WindowsPrincipal(WindowsIdentity.GetCurrent())
      m_CurrentUser = wp.Identity.Name
      If m_CurrentUser Is Nothing Then _
         Throw New Exception("Could not retrieve " & _
          "the current user's name.")

      ' set caption
      Me.Text = "Customers - Current User: " & _
      m_CurrentUser

      ' Populate the soap header with the auth info
      m_AuthHeader.Username = m_CurrentUser
      m_AuthHeader.Password = "passs"
      m_AuthHeader.IsAuthenticated = _
      wp.Identity.IsAuthenticated
      m_CustWebSrv.AuthHeaderValue = m_AuthHeader

      ' Fill the customer list box 
      FillCustList()

      Catch Exc As Exception
         ' Reset the cursor and display error message
         Me.Cursor = Cursors.Default
         MsgBox("Error Initializing the application." & _
          vbCrLf & "System Error: " & Exc.Message)
   End Try
End Sub

The FillCustList method in turn calls the BeginGetCustomer method of the Web service. This is the asynchronous version of the GetCustomer method. A pointer to the callback function (FillCustListCallBack) is passed to the method using the AddressOf operator within a new instance of the AsyncCallback class.

Private Sub FillCustList()

   ' Called by Delete, Add and Form Load events
   Try
      ' Set the cursor
      Me.Cursor = Cursors.WaitCursor

      ' Get the calls made so far
      m_CustWebSrv.BeginGetCustomer("", _
       New AsyncCallback(AddressOf _
       Me.FillCustListCallBack), Nothing)

      Catch SoapException As SoapException
         MsgBox("FillCustList Error: " & _
          SoapException.Message)
         ' Reset the cursor 
         Me.Cursor = Cursors.Default
      Catch Exc As Exception
         MsgBox("FillCustList Error: " & Exc.Message)
         ' Reset the cursor 
         Me.Cursor = Cursors.Default
   End Try
End Sub

When the Web service method finishes processing the BeginGetCustomer request, the FillCustListCallback function is called. Within this function, the EndGetCustomer method is called to return any results processed by the Web service. This is the asynchronous counterpart to the BeginGetCustomer method.

Public Sub FillCustListCallBack(ByVal ar As IAsyncResult)

   Dim dsCust As DataSet = Nothing
   Dim dRow As DataRow = Nothing

   Try
      ' Set the cursor
      Me.Cursor = Cursors.WaitCursor

      ' Get the list of customers
      dsCust = m_CustWebSrv.EndGetCustomer(ar)
      If dsCust Is Nothing Then _
       Throw New Exception("There was no customer " & _
       "information returned.")

      ' Clear the list box and fill it
      lstCust.Items.Clear()

      ' Loop through and populate list box.
      For Each dRow In dsCust.Tables(0).Rows
         lstCust.Items.Add(dRow.Item(0).ToString)
      Next

      Catch SoapException As SoapException
         MsgBox("FillCustListCallBack Error: " & _
          SoapException.Message)
      Catch Exc As Exception
         MsgBox("FillCustListCallBack Error: " & _
          Exc.Message)
      Finally
         ' Reset the cursor 
         Me.Cursor = Cursors.Default
      End Try
End Sub

To finish the client application, copy the rest of the source code (listed below) into the frmMain.vb module and compile the client application into the DemoWebClient.exe.

' This is the event handles for the button controls
Private Sub CustomerEventHandler( _
 ByVal eventSender As System.Object, _
 ByVal eventArgs As System.EventArgs) _
 Handles cmdDelete.Click, cmdAdd.Click, _
 cmdClear.Click, cmdUpdate.Click, cmdCancel.Click

   Dim CustomerID As String = Nothing
   Dim ctl As Control = Nothing

   Try
      ' set the cursor to the hourglass
      Me.Cursor = Cursors.WaitCursor

      ' Cast the sender object into a control
      ctl = CType(eventSender, Control)

      ' Process according to the button selected
      Select Case ctl.Name
         Case "cmdDelete"
            If Not (lstCust.SelectedItem Is Nothing) Then
               CustomerID = lstCust.SelectedItem.ToString

               ' Delete the current customer 
               ' asynchronously
               m_CustWebSrv.BeginDelete(CustomerID, _
                New AsyncCallback(AddressOf _
                Me.DeleteCustCallBack),Nothing)
            End If
         Case "cmdClear"
            ' Clear the form
            ClearCustInfo()
         Case "cmdCancel"
            ' close the application
            Me.Close()
         Case "cmdAdd"
            ' Add the customer aysnc
            m_CustWebSrv.BeginAdd(txtCustomerID.Text, _
             txtCompany.Text, txtContactName.Text, _
             txtContactTitle.Text, txtAddress.Text, _
             txtCity.Text, txtRegion.Text, _
             txtPostalCode.Text, txtCountry.Text, _
             "", "", New AsyncCallback(AddressOf _
             Me.AddCustCallBack), Nothing)

         Case "cmdUpdate"
            ' Retrive the customer id and update.
            If Not (lstCust.SelectedItem Is Nothing) Then
               CustomerID = lstCust.SelectedItem.ToString

               ' Update the customer asynchronously
               m_CustWebSrv.BeginUpdate(CustomerID, _
                txtCompany.Text, txtContactName.Text, _
                txtContactTitle.Text, txtAddress.Text, _
                txtCity.Text, txtRegion.Text, _
                txtPostalCode.Text, txtCountry.Text, _
                "", "", New AsyncCallback(AddressOf _
                Me.UpdateCustCallBack), Nothing)
            End If
         Case Else
            MsgBox("The current button does not " & _
             "do anything yet.")
      End Select
      Catch Exc As Exception
         MsgBox(Exc.Message)
      Finally
         ' Set the cursor back to default
         Me.Cursor = Cursors.Default
   End Try
End Sub

' This is the event handler for the selected index changed ' event of the 
      listbox
Private Sub CustListEventHandler(_
 ByVal eventSender As System.Object, _
 ByVal eventArgs As System.EventArgs) _
 Handles lstCust.SelectedIndexChanged

   Dim CustomerID As String = Nothing

   ' Get the list of customers
   If Not (lstCust.SelectedItem Is Nothing) Then
      Try
         ' Set the cursor
         Me.Cursor = Cursors.WaitCursor

         CustomerID = lstCust.SelectedItem.ToString

         ' Retrieve the info
         m_CustWebSrv.BeginGetCustomer(CustomerID, New _
          AsyncCallback(AddressOf Me.CustListCallBack), _
          Nothing)

         Catch SoapException As SoapException
            MsgBox("Unable to retrieve the " & _
             "information for '" & CustomerID & "'." & _
             vbCrLf & "System Error: " & _
             SoapException.Message)
            Me.Cursor = Cursors.Default
         Catch Exc As Exception
            MsgBox("Unable to retrieve the " & _
             "information for '" & CustomerID & "'." & _
             vbCrLf & "System Error: " & _
             Exc.Message)
            Me.Cursor = Cursors.Default
      End Try
   End If
End Sub

' These are all the call backs used for the async calls.
Public Sub UpdateCustCallBack(ByVal ar As IAsyncResult)

   Try
      ' set the cursor
      Me.Cursor = Cursors.WaitCursor

      m_CustWebSrv.EndUpdate(ar)

      Catch SoapException As SoapException
         MsgBox("Unable to update the information " & 
          "for the customer." & vbCrLf & _
          "System Error: " & SoapException.Message)
      Catch Exc As Exception
         MsgBox(Exc.Message)
      Finally
         Me.Cursor = Cursors.Default
   End Try
End Sub

Public Sub DeleteCustCallBack(ByVal ar As IAsyncResult)

   Try
      ' set the cursor
      Me.Cursor = Cursors.WaitCursor

      m_CustWebSrv.EndDelete(ar)

      ' Clear the form
      ClearCustInfo()

      ' Refill the list
      FillCustList()

      Catch SoapException As SoapException
         MsgBox("Unable to delete the information " & _
          "for the customer." & vbCrLf & _
          "System Error: " & SoapException.Message)
      Catch Exc As Exception
         MsgBox(Exc.Message)
      Finally
         ' Reset the cursor
         Me.Cursor = Cursors.Default
   End Try
End Sub

Public Sub AddCustCallBack(ByVal ar As IAsyncResult)

   Dim CustIndex As System.Int32 = 0

   Try
      ' Set the cursor
      Me.Cursor = Cursors.WaitCursor

      ' Add the customer
      m_CustWebSrv.EndAdd(ar)

      ' Refill the list
      FillCustList()

      ' Search for the just added customer and select it
      CustIndex = lstCust.FindString(txtCustomerID.Text)
      If CustIndex > -1 Then _
       lstCust.SetSelected(CustIndex, True)

      Catch SoapException As SoapException
         ' Just display message box with info
         MsgBox("Unable to Add the '" & _
          txtCustomerID.Text & _
          "' to the customer database." & vbCrLf & _
          "System Error: " & SoapException.Message)

      Catch Exc As Exception
         MsgBox(Exc.Message)
      Finally
         ' Reset the cursor
         Me.Cursor = Cursors.Default
   End Try
End Sub

Public Sub CustListCallBack(ByVal ar As IAsyncResult)

   Dim dsCust As DataSet = Nothing
   Dim dRow As DataRow = Nothing

   ' Get the list of customers
   Try
      ' Set the cursor
      Me.Cursor = Cursors.WaitCursor

      ' Retrieve the info
      dsCust = m_CustWebSrv.EndGetCustomer(ar)
      If dsCust Is Nothing Then _
       Throw New Exception("There was no customer " & _
       "information returned.")

      If dsCust.Tables(0).Rows.Count < 1 Then _
       Throw New Exception("There was no customer " & _
       "information returned for the customer.")
      dRow = dsCust.Tables(0).Rows(0)

      ' Populate the form
      FillCustomerInfo(dRow)

      ' get the calls made so far
      m_CustWebSrv.BeginCallsPerUser(New _
       AsyncCallback(AddressOf _
       Me.CallsPerUserCallBack), Nothing)

      Catch SoapException As SoapException
         MsgBox("CustListCallBack Error: " & _
          SoapException.Message)
      Catch Exc As Exception
         MsgBox("CustListCallBack Error: " & Exc.Message)
      Finally
         ' Reset tcursor
         Me.Cursor = Cursors.Default
   End Try
End Sub

Private Sub FillCustomerInfo(ByVal oCust As DataRow)

   If Not oCust Is Nothing Then
      ' Fill in the form
      txtCustomerID.Text = Trim$(oCust.Item(0).ToString)
      txtCompany.Text = Trim$(oCust.Item(1).ToString)
      txtContactName.Text = Trim$(oCust.Item(2).ToString)
      txtContactTitle.Text = Trim$(oCust.Item(3).ToString)
      txtAddress.Text = Trim$(oCust.Item(4).ToString)
      txtCity.Text = Trim$(oCust.Item(5).ToString)
      txtRegion.Text = Trim$(oCust.Item(6).ToString)
      txtPostalCode.Text = Trim$(oCust.Item(7).ToString)
      txtCountry.Text = Trim$(oCust.Item(8).ToString)
   End If
End Sub

Private Sub ClearCustInfo()
   Dim ctl As Control = Nothing

   ' Clear all the text boxes.
   For Each ctl In Me.Controls
      If TypeOf ctl Is TextBox Then _
       ctl.Text = Nothing
   Next
End Sub

Summary

XML Web services provide a flexible and robust infrastructure for the creation and consumption of Web services. Web services can be created easily and can be extended with the .NET Framework and Visual Studio .NET. Most developers will no longer have to learn the intricacies of underlying infrastructure because .NET does it all. And nothing can be easier than creating client applications that consume Web services using Visual Studio .NET. The client-side proxies are generated automatically, hiding the details of the marshalling calls from the developer. Accessing a Web service method is as easy as it was accessing an object method in COM, except without the pain of registration, type libraries, and binary compatibility issues.

The .NET Framework and XML Web services technologies will prove to be empowering tools for your development efforts going forward.

About the Author

Marty Wasznicky is a Senior Systems Engineer with Microsoft Corporation in the Southern California district, focusing on Enterprise Application Integration, Business to Business and E-Commerce. He has more than ten years of experience architecting and implementing solutions in the IT industry in both corporate and consulting capacities. He is a Microsoft Certified Solutions Developer, Microsoft Certified Systems Engineer, Microsoft Certified Database Administrator and Certified Novell Engineer. He has authored numerous articles in the Visual Basic Programmer's Journal and the Access/VB/SQL Advisor magazines.

About Informant Communications Group

Informant Communications Group, Inc. (www.informant.com) is a diversified media company focused on the information technology sector. Specializing in software development publications, conferences, catalog publishing and Web sites, ICG was founded in 1990. With offices in the United States and the United Kingdom, ICG has served as a respected media and marketing content integrator, satisfying the needs of IT professionals for quality technical information.

Copyright © 2002 Informant Communications Group and Microsoft Corporation

Technical editing: PDSA, Inc. or KNG Consulting