Вы находитесь на странице: 1из 38

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).
Copy
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.
Copy
<% 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.
Copy
<% 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.
Copy
<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.
Copy
< 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.
Copy
' 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).
Copy
' 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.
Copy
' 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 http://localhost/DemoWebSrv.
Visual Studio .NET will create an IIS virtual directory pointing to the physical location. The
virtual directory URL will be http://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:
Copy
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
Copy
Dim custDataSet as System.Data.DataSet
Use the following declaration instead:
Copy
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.
Copy
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.
Copy
#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.
Copy
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.
Copy
<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.
Copy
<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
http://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:
Copy
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
(http://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.
Copy
' 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
Copy
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.
Copy
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.
Copy
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.
Copy
' 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.