The CustomersOrders Web Service

Team-Fly team-fly    

 
ADO.NET Programming in Visual Basic .NET
By Steve  Holzner, Bob  Howell

Table of Contents
Chapter 10.   Building XML Web Services


Since we have been working with the customers/orders/order details structure, let's continue with that and build a Web Service that returns and updates data about customer orders.

Let's rename our Web Service from the default to CustomersOrders. Do this by opening the page in design view. Web Services in Visual Studio are built using the Component Model, so we get a visual designer to work with. The only property available is the Name property. Change it from Service1 to CustomersOrders. Now change the file name to CustomersOrders as well. We can rename the file ins the usual manner using the Solution Explorer.

Since the Web Service class is derived from the ComponentModel class, we can add components using a visual designer. Before we can build our Web Service methods , we must first create all of the data components . We will need one SqlConnection object, four SqlDataAdapter objects, and three DataSet objects. All three DataSets will be strongly typed.

We will not go through the step-by-step details of creating the data components. By now you should be able to do that on your own. Here is how the components should be set up. Feel free to use the wizards and builders provided to speed the process.

  • SqlConnection1Connect to a convenient Northwind database.

     SqlDataAdapter1Standard setup using the following Select  statement:  SELECT     CustomerID, CompanyName, ContactName, ContactTitle, Address,  City, Region, PostalCode, Country, Phone, Fax, BirthDate  FROM         Customers  WHERE     (CustomerID = @CustomerID) 
  • SqlDataAdapter2Read-only setup. (Click Advanced tab and disable generation of Update, Insert, and Delete statements.) Use the following SQL statement:

     SELECT     CustomerID, CompanyName  FROM         Customers 
  • SqlDataAdapter3Standard setup. Use the following SQL statement:

     SELECT     OrderID, CustomerID, EmployeeID, OrderDate, RequiredDate,  ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity,  ShipRegion,                        ShipPostalCode, ShipCountry  FROM         Orders  WHERE     (CustomerID = @CustomerID) 
  • SqlDataAdapter4Standard setup. Use the following SQL statement:

     SELECT     OrderID, ProductID, UnitPrice, Quantity, Discount             FROM         [Order Details]             WHERE     (OrderID IN                                       (SELECT     OrderID                                         FROM          Orders                              WHERE      CustomerID = @CustomerID)) 
  • DsCust1 DataSetCreate from SqlDataAdapter1.

  • DsCustList1 DataSet Create from SqlDataAdapter2.

  • DsOrders1 DataSetCreate from SqlDataAdapters 3 and 4. Create the relation between the Orders and Order Detail table the way we did in the previous chapter.

  • In each of the three DataSets, add one more table called Errors. (Hint: Use the schema editor to do this.) The table has two columns , ErrorText and StackTrace, both strings. These tables will be used for error reporting; we'll see why we need to do this later.

Now it's time to begin adding our web methods. Remember the pretext; each web method must be self-contained and not rely on any outside data or services. By adding the data components to the visual designer they become module-level objects, but we are not using them to persist data between requests . They must either be passed in from the client (for the DataSet only) or recreated each time a web method is called.

The first web method we will create will be used to return our list of customers for populating the list portion of the combo box. The code looks like this:

 <WebMethod(Description:="Gets List of Customers for Combo Box.")> _     Public Function GetCustomerList() As dsCustList       DsCustList1 = New dsCustList()       Try         SqlDataAdapter2.Fill(DsCustList1)       Catch errobj As Exception         DsCustList1.Errors.Rows.Add(New String() {errobj.Message,  errobj.StackTrace})       End Try       Return DsCustList1     End Function 

This is the simplest of our methods. Notice the attribute setting before the function declaration. In order to be seen by the client, the web method must use this attribute. The Description argument sets the description that will display for this method when doing a UDDI query. Other than that, it is a pretty normal-looking function. Notice that we recreate the DataSet at the beginning of the function.

Let's compile and run the Web Service. Yes, we can run a Web Service in much the same way as we can run a Web Form. But the service has no UI. That's okay. Visual Studio will generate one for us (see Figure 10.3).

Figure 10.3. The Web Service introductory page.

graphics/10fig03.gif

Incidentally, the description of the Web Service itself is set in the attribute on the declaration of the class. Here is the code for setting the description:

 Imports System.Web.Services  <System.Web.Services.WebService(Namespace:="http://  www.emanageservice.com/", Description:="Returns and updates data about  customers' orders.")> _  Public Class CustomersOrders    Inherits System.Web.Services.WebService 

The great thing about this page is that we can use it to test the web method. If we click on the method, we get the screen shown in Figure 10.4.

Figure 10.4. The method invocation screen.

graphics/10fig04.gif

The screen virtually gives you the HTML code to invoke the method via different protocols. It also enables you to test the method by invoking it directly and then viewing the results. Not all methods can be invoked this way; they must be able to be called using the HTTP Get method. Still, it makes a great debugging aid. You can set breakpoints in your code and use this to invoke your method before using it in your production version. When you run the method, it returns raw XML as in Figure 10.5.

Figure 10.5. XML results from method invoke.

graphics/10fig05.jpg

Let's add the rest of our methods. We need two more methods for now. They are:

  • GetCustomer Pass it a CustomerID and it returns a DataSet with customer details.

  • GetOrders Pass it a CustomerID and it returns all the orders and order details for the customer.

Here is the code for GetCustomer:

 <WebMethod(Description:="Gets Details about a Customer.")> _     Public Function GetCustomer(ByVal strCustID As String) As dsCust       DsCust1 = New dsCust()       If strCustID = "" Then         DsCust1.Errors.Rows.Add(New String() {"Customer ID is Required",  ""})         Return DsCust1       End If       Try         SqlDataAdapter1.SelectCommand.Parameters("@CustomerID").Value =  strCustID         SqlDataAdapter1.Fill(DsCust1)       Catch errobj As Exception         DsCust1.Errors.Rows.Add(New String() {errobj.Message,  errobj.StackTrace})       End Try       Return DsCust1     End Function 

Handling Errors in Web Services

Since the CustomerID is required, the first thing we do is check to see if the caller passed a string that is not empty. If the caller did, we return an error. Now this is interesting. Error handling in a Web Service is a bit tricky because of the stateless nature of the Web. In a regular component (not a Web Service) we would throw the error back to the client or raise an event or store the error message in a module-level variable for the client to obtain later. But in a Web Service we have no such luxury. If we try to throw the error back to the client, all we get is an error page, or if it is a client application, it is ignored. We can't raise events in Web Services and we can't store the value for later retrieval. That is a bit of a kludge anyway, a holdover from the pre-ActiveX DLL days.

The best way to handle errors in a Web Service is via the DataSet itself. I include an Errors table in all my DataSets that will be used in Web Services. When I encounter an error, I simply fill the Errors table with the error message and optionally the stack trace. Then I return the DataSet to the client. It is then up to the client to interrogate the table to see if any errors occurred.

If you just throw the error back to the client, all you get is the default page shown in Figure 10.6.

Figure 10.6. The error page when you throw an exception.

graphics/10fig06.gif

When you use a table to report the error, you can see what went wrong and even get the list number of the line that caused the problem. In a production environment, you might want to return less information to the client, but log the detailed information on the server for debugging purposes. Figure 10.7 shows what you get when you use the Errors table.

Figure 10.7. The XML error table returned after an error.

graphics/10fig07.jpg

As you can see, you can trace the error right down to the line number. Isn't that more helpful than HTTP 500Internal Server Error? By the way, just for this demonstration I renamed the CustomerID column CustomerIDX so it would throw the error. If you want to try this example, make sure you rename it properly when you are done.

Getting Multitable DataSets

The Orders/Order Details relational structure is a little more complicated. Let's look at the code to retrieve the orders for a customer. Remember in our DataSet we defined the Orders table as the master table and the Order Details table as the detail table.

 <WebMethod(Description:="Gets Orders and Details for a Customer")> _     Public Function GetOrders(ByVal strCustID As String) As dsOrders       DsOrders1 = New dsOrders()       If strCustID = "" Then         DsCust1.Errors.Rows.Add(New String() {"Customer ID is Required",  ""})         Return DsOrders1       End If       Try         SqlDataAdapter3.SelectCommand.Parameters("@CustomerID").Value =  strCustID         SqlDataAdapter3.Fill(DsOrders1.Orders)         SqlDataAdapter4.SelectCommand.Parameters("@CustomerID").Value =  strCustID         SqlDataAdapter4.Fill(DsOrders1.Order_Details)       Catch errobj As Exception         DsOrders1.Errors.Rows.Add(New String() {errobj.Message,  errobj.StackTrace})       End Try       Return DsOrders1     End Function 

As with our example in the previous chapter, we retrieve all of the orders and the details for all of the orders. We then let the relational constraint we built enforce which details go with which orders. This is different than in the past when you would query the database each time the user changed rows in the master table. As a refresher, here are the SQL statements for the SelectCommands of the two DataAdapters.

SqlDataAdapter3:

 SELECT     OrderID, CustomerID, EmployeeID, OrderDate, RequiredDate,  ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity,  ShipRegion,                        ShipPostalCode, ShipCountry  FROM         Orders  WHERE     (CustomerID = @CustomerID) 

SqlDataAdapter4:

 SELECT     OrderID, ProductID, UnitPrice, Quantity, Discount             FROM         [Order Details]             WHERE     (OrderID IN                                       (SELECT     OrderID                                         FROM          Orders                              WHERE      CustomerID = @CustomerID)) 

When we execute the web method through the test interface, we get all of the order details first, then all of the order headers. Since the list is quite long I will only show excerpts. Figure 10.8 shows the order detail metadata information:

Figure 10.8. Order detail metadata.

graphics/10fig08.jpg

In Figure 10.9 we can see some of the order details data.

Figure 10.9. Order details data.

graphics/10fig09.jpg

Figure 10.10 shows the order header metadata.

Figure 10.10. The order header metadata.

graphics/10fig10.jpg

Figure 10.11 shows the order header data. Notice the scroll bar which is toward the end of the file.

Figure 10.11. Order header data.

graphics/10fig11.jpg

Updating Data with a Web Service

There are two main approaches to updating the database with a Web Service.

The first approach involves passing the DataSet back to the server. Of course, you would only want to pass rows that have changed, been inserted, or been deleted. You can do this with the GetChanges method. This would be the preferred method when using a Windows Forms client and you know your Web Service will be used only by Microsoft .NET Windows Forms clients .

The second approach, which is to pass the changed values back to the server, then let the server handle updating the database and is best if your Web Service is intended for general consumption by both Microsoft .NET and other platforms, which may not support DataSets, or if you will use a nonASP .NET browser-based client.

You can't go wrong with the second method, because it will be able to handle any client platform. It does make the update a little more complex though. We will demonstrate both methods.

Passing a DataSet Back to the Web Service

Since the DataSet can be serialized as XML, it can be passed back to the Web Service. We cannot, however, use HTTP post or get. We must use a SOAP request. Because of this, we cannot use the test page to test the method. Here is the code for accepting a DataSet as a method parameter:

 <WebMethod(Description:="Updates Details about a Customer. Form 1.  Pass It The DataSet.")> _      Public Function UpdateCustomer1(ByVal dsCust1 As dsCust) As dsCust          Try              dsCust1.Errors.Rows.Clear()              If dsCust1.HasChanges Then                  SqlDataAdapter1.Update(dsCust1)              End If          Catch errobj As Exception              dsCust1.Errors.Rows.Add(New String() {errobj.Message,  errobj.StackTrace})          End Try          Return dsCust1      End Function 

The advantage of this way of updating the database is that the code is fairly simple. Essentially you are getting a DataSet, then calling the Update method of the DataAdapter. We must first clear the Errors table in case we passed an error to the client. Also, before we invoke the method on the client side, we should call the GetChanges method of the DataSet so we don't pass too much data back to the server. Despite advances in technology, there are still many 56 KB dial-up connections in use. These typically download at 56 KB but only send at 28.8 or worse , 14.4. By limiting the amount of data you have to send back to the server you will do these users a big favor. We will see how to do this later when we build our client.

Passing Values to the Server

To make sure your Web Service can be consumed by any user on the Web (if that is your requirement) you must pass only basic data types back and forth. We didn't cover this in the earlier section on getting data, but this is true there as well. If our Web Service is to be consumed by non.NET clients, we can't pass a DataSet down to the client either. It will not know what to do with it. We would have to pass comma-delimited strings or some other format that is universally readable.

 Here is the code to pass values back to the service:      <WebMethod(Description:="Updates Details about a Customer. Form 2.  Pass It The Values.")> _      Public Function UpdateCustomer2( _       ByVal strCustID As String, _       ByVal strCompanyName As String, _       ByVal strContactName As String, _       ByVal strContactTitle As String, _       ByVal strAddress As String, _       ByVal strCity As String, _       ByVal strState As String, _       ByVal strPostal As String, _       ByVal strPhone As String) As dsCust          DsCust1 = New dsCust()          If strCustID = "" Then              DsCust1.Errors.Rows.Add(New String() {"Customer ID is  Required", ""})              Return DsCust1          End If          Try              DsCust1.Errors.Rows.Clear()  SqlDataAdapter1.SelectCommand.Parameters("@CustomerID").Value =  strCustID              SqlDataAdapter1.Fill(DsCust1)              DsCust1.Customers.Rows(0)("CustomerID") = strCustID              DsCust1.Customers.Rows(0)("CompanyName") = strCompanyName              DsCust1.Customers.Rows(0)("ContactName") = strContactName              DsCust1.Customers.Rows(0)("ContactTitle") = strContactTitle              DsCust1.Customers.Rows(0)("Address") = strAddress              DsCust1.Customers.Rows(0)("City") = strCity              DsCust1.Customers.Rows(0)("Region") = strState              DsCust1.Customers.Rows(0)("PostalCode") = strPostal              DsCust1.Customers.Rows(0)("Phone") = strPhone              If DsCust1.HasChanges Then                  SqlDataAdapter1.Update(DsCust1)              End If          Catch errobj As Exception              DsCust1.Errors.Rows.Add(New String() {errobj.Message,  errobj.StackTrace})          End Try          Return DsCust1      End Function 

As you can see, the code is more complex. Also, you can only update one row at a time with UpdateCustomer2, whereas the UpdateCustomer1 will push multiple updates, as well as inserts and deletes, to the database. The advantage to the UPdateCustomer2 is that it is entirely self-contained and does not rely on any external data structures. Notice that we actually create the DataSet using the passed-in CustomerID, then update it. One thing we don't check is to see if the CustomerID still exists. If someone deleted this customer, we would not have anything to update. This will throw an error; however, we would probably want to handle this condition with a little more grace.

You could also create a custom collection of classes that can be passed back and forth using XML. This would create a custom schema that any system capable of understanding XML would be able to use.

We would use similar techniques to update the orders and order details. For the sake of space, we will not go into detail about how to do this, but the same considerations apply. For the orders and details, it is definitely better to be able to pass the DataSet with only the changes back to the client. If you must do it using values, and you must pass row by row, then you will have to make multiple calls to the web method, which has its own negative issues. It is always better to use .NET clients with a .NET Web Service. You could also provide both methods and instruct your clients to use the .NET-specific methods if they are using a .NET client or to use the other methods if not.


Team-Fly team-fly    
Top


ADO. NET Programming in Visual Basic. NET
ADO.NET Programming in Visual Basic .NET (2nd Edition)
ISBN: 0131018817
EAN: 2147483647
Year: 2005
Pages: 123

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net