zoukankan      html  css  js  c++  java
  • 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

    PropertiesDescription
    BufferResponseGets or sets whether the response for this request is buffered
    CacheDurationGets 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.
    DescriptionA message describing the Web service method. A listing in the WSDL file
    EnableSessionIndicates whether session state is enabled for a Web service method
    MessageNameThe name used for the Web service method in the data passed to and returned from a Web service method. Useful for exposing overloaded functions
    TransactionOptionIndicates 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:

    MethodDescription
    GetCustomerOptionally accepts the customer ID and returns either a specific customer record or all customer records in a disconnected ADODB.Recordset
    AddAdds a new customer to the Northwind database
    DeleteDeletes only newly added customers from the Northwind database based on Customer ID
    UpdateUpdates 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.

    ControlType
    cmdClearCommand Button
    cmdUpdateCommand Button
    cmdAddCommand Button
    cmdDeleteCommand Button
    txtCustomerIDText Box
    txtCompanyText Box
    txtContactNameText Box
    txtContactTitleText Box
    txtAddressText Box
    txtCityText Box
    txtRegionText Box
    txtPostalCodeText Box
    txtCountryText Box
    lstCustList 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 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:

    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).

    MethodDescription
    GetCustomerOptionally accepts the customer ID and returns either a specific customer record or all customer records in a System.Data.DataSet
    AddAdds a new customer to the Northwind database
    DeleteDeletes only newly added customers from the Northwind database based on Customer ID
    UpdateUpdates 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 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:

    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.

    ControlType
    cmdCancelButton
    cmdClearButton
    cmdUpdateButton
    cmdAddButton
    cmdDeleteButton
    txtCustomerIDText Box
    txtCompanyText Box
    txtContactNameText Box
    txtContactTitleText Box
    txtAddressText Box
    txtCityText Box
    txtRegionText Box
    txtPostalCodeText Box
    txtCountryText Box
    lstCustList 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 clikckingAdd 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.

    ' 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 theAccess/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

  • 相关阅读:
    一个使用 Python 的人工智能聊天机器人框架
    【TensorFlow 官网 可以直接访问】让中国开发者更容易地使用TensorFlow打造人工智能应用
    Object Relational Tutorial 对象关系教程
    Automap sqlalchemy.ext.automap 自动映射数据库表结构
    回溯法
    子集树和排列树
    平衡树
    二叉查找树
    graphviz使用
    linux进程内存布局
  • 原文地址:https://www.cnblogs.com/jjkv3/p/2540261.html
Copyright © 2011-2022 走看看