Using Web Services with Windows Forms Clients

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


Using Web Services with Windows Forms Clients

You can consume Web Services with Windows Forms clients. This is the most robust way of using Web Services. You have the advantage of using HTTP protocol for simplicity, yet you have all the advantages of using Windows Forms to build a robust UI.

The way we do this is almost as simple as referencing a local component. We create a reference to a Web Service. But first let's create an empty Windows Forms project and add it to the solution. Call the new project ADOBook10-02. Once the project is created we can create our web reference. This is a little different than creating a reference to a local component. To create the web reference, we point to the .asmx document on the server via a URL. Do this by right-clicking on the References node of the ADOBook10-02 project in the Project Explorer. Then select Create Web Reference as in Figure 10.12.

Figure 10.12. Adding a web reference.

graphics/10fig12.gif

When you select this item, the Web Reference dialog opens. This combines the UDDI service with the intro page we saw previously (see Figure 10.13).

Figure 10.13. The Add Web Service dialog.

graphics/10fig13.gif

To add our service to the project, we must know where to point the dialog. Since we have not added it to UDDI service, we cannot simply browse for it. Enter the full URL to the service in the address box, as shown in Figure 10.14.

Figure 10.14. The URL of the Web Service.

graphics/10fig14.gif

If you have entered it correctly, you should bring up the intro page as in Figure 10.15.

Figure 10.15. The Add Web Reference dialog with the Web Service found.

graphics/10fig15.gif

Once you have determined that this is the correct Web Service, simple click Add Reference to make it available in your project. Once you have added the reference, it appears in the Solution Explorer as in Figure 10.16.

Figure 10.16. The Web Service reference.

graphics/10fig16.gif

That little mouse click did a lot of work. To create the reference, it created all of the files you see under the localhost node, plus some you don't see. The CustomersOrders.wsdl document describes the entire interface to the Web Service. Remember we said that the WSDL document is the analog of the COM type library? If we open it we will see that it is an XML document that describes the interface in three ways, HTTP get, HTTP post, and SOAP.

Second, it also downloaded the three DataSet schemas. This is so we have access to the data types locally.

Third, it creates the Reference.map document. This document is the discovery reference, which contains references to the other documents. Open it and look at it to see what we mean. It is straightforward.

Fourth, it creates a Visual Basic interface proxy class. It does this from the .map file and the WSDL document. This is a Visual Basic source file that enables us to call the Web Service methods as if they were local function calls. It wraps the calls to the Web Service for us.

Take a look at this file, but don't make any changes to it. Notice the use of the Invoke method. This is how we call late-bound classes in .NET. This is analogous to the CallByName function we had in VB 6. This class is not technically necessary; we don't have to wrap the method calls like this. But then we would have made our Web Service method calls using late-binding the way the proxy class does, and it is a lot more work. Why bother? Let Visual Studio do it for you. If, for whatever reason, you were creating a generic thin client, you could use the late-bound calls after parsing the WSDL document yourself.

Let's put our Web Service to work. First step is to create our UI. We'll use a Windows Forms version of the Web Forms project we did in the previous chapter. You will have a combo box that is populated with the list of companies, and a details area that has the information about the selected company. We will have an Update button to allow changes to the details.

Now, let's add our DataSets to the form. Since we are using a Web Service, we do not need a Connection object or DataAdapters. Already we can see a benefit to using a Web Service. The client does not know about or care what kind of database we are using or where the database is located. This is a great security benefit, as well as a benefit to your application programmers. End users or application programmers do not have to have any database training or even know where the database is.

When we add a DataSet, we will use typed DataSets, but we will get our types from the Web Service. This is really neat. Remember how it downloaded the XSD schema files for the DataSets? We can use these to create our own typed DataSets locally so when we pass DataSets back and forth to the Web Service, they are strongly typed. Start by dragging a DataSet from the toolbox and dropping it in the form. The DataSet dialog opens. In the drop down under Typed DataSet, you will see the DataSets from the Web Service listed. Select the one shown in Figure 10.17.

Figure 10.17. Creating a typed DataSet from a Web Service.

graphics/10fig17.gif

This creates DsCustList1. We will use this to bind to the list portion of the ComboBox. Next create another DataSet, this time use dsCust as the type. Don't create one for DsOrders yet.

Once we have created the DataSets, we will add a DataView object to bind our controls to. Drag one onto the form and set its DataTable property to DsCust1.Customers. Remember, when we bind to a DataTable, we are really binding to the default view, so instead we will use our own view. This is a best practice for general use. For the sake of space I will show you only the interface; by now we should be pretty adept at creating a Windows Form. Create the form to look like Figure 10.18. Bind the text boxes to their appropriate fields in the DataView1 object.

Figure 10.18. The UI for the Web Service Form.

graphics/10fig18.jpg

This is essentially the same thing we had with the Web Form, except as a Windows Form. The code to create the Web Service and call the methods for getting data follows :

 Private mWebSvc As localhost.CustomersOrders = New  localhost.CustomersOrders()      Private Sub frmCust_Load(ByVal sender As System.Object, ByVal e As  System.EventArgs) Handles MyBase.Load          DsCustList1.Merge(mWebSvc.GetCustomerList())      End Sub      Private Sub ComboBox1_SelectedIndexChanged(ByVal sender As  System.Object, ByVal e As System.EventArgs) Handles  ComboBox1.SelectedIndexChanged          DsCust1.Clear()          DsCust1.Merge(mWebSvc.GetCustomer(ComboBox1.SelectedValue))      End Sub 

Notice that instead of assigning the output of the web method to the DataSet, I use the Merge method on the DataSet. I do this so I don't have to rebind the DataSet to all the text boxes. Think about it. We have created our DataSets using the visual designer. That means that instances of them are created automatically when the form starts. When we bind the controls to the DataSets, it is to this instance that we are binding. If we were to simply assign the DataSets to the output of the web methods, we would be effectively destroying the bound instance and creating an instance that is not bound to anything. By using the Merge method, we do not have to destroy the bound instance of the DataSets, and the bindings remain intact. This is true even though we are binding through a DataView. This has nothing to do with Web Services, by the way. It is just as true if we were using local data components .

We can declare our Web Service as a module-level variable. With each method invocation, the proxy class handles creating the web request and returning the data as the correct type. So even though it seems as though the Web Service is existing for the whole lifetime of the client program, it really is being created and destroyed with every method call. What does exist is the proxy class. But this is all transparent to the client application. When we compile and run the client program, we have the same functionality that we had with the Web Form. You can begin to see how by using a Web Service we can build equivalent Windows and web front ends to the same business rules fairly easily, compared to previous technologies. Figure 10.19 shows the form in action.

Figure 10.19. The Web Service Windows client.

graphics/10fig19.gif

Now let's implement the Update button. For now, we'll just pass the DataSet back to the server. Since we made the web method return the DataSet as well, we can take advantage of the SqlDataAdapter2's refresh of the DataSet to keep the display current.

Here is the code to do a simple update:

 Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As  System.EventArgs) Handles Button1.Click      Dim dsTemp As localhost.dsCust      Me.BindingContext(DataView1).EndCurrentEdit()      If DsCust1.HasChanges(DataRowState.Modified) Then        dsTemp = DsCust1.GetChanges(DataRowState.Modified)        DsCust1.Merge(mWebSvc.UpdateCustomer1(dsTemp))      End If    End Sub 

First, as usual, we end the pending edit. Next we see if there were any changes. If there were, we copy them to a temporary DataSet and send the temporary DataSet off to the web method. We get back another DataSet which we then merge (remember our discussion earlier) into the permanent DataSet.

Now let's add some complexity. We want to create a way of adding records to the table. Do we need a new web method? The answer after this: The first and most obvious thing we need to do is add two more buttons to the form. Label one New and the other Cancel Add. Set the Enabled property of the Cancel button to False. We want the Cancel button enabled only in Add mode. Okay, do we need a new method? No, we do not. What we already have will suffice.

As with our Web Form project in the last chapter, when the user clicks the New button, the form will clear and allow the user to enter a new record. The user can cancel the add at any time by clicking the Cancel button. The new form is shown in Figure 10.20.

Figure 10.20. The finished form.

graphics/10fig20.jpg

Let's do the code for the New button first since it's the simplest. Here we must clear the DataSet, put the DataView into Add mode, and enable the CustomerID text box (TextBox1). We also want to disable the combo box. The user should not select another customer until he is finished adding the new one. We then want to enable the Cancel button. Here is the code for the New button and the Cancel button:

 Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As  System.EventArgs) Handles Button2.Click      DsCust1.Clear()      DataView1.AddNew()      ComboBox1.Enabled = False      TextBox1.ReadOnly = False      Button3.Enabled = True      Button2.Enabled = False    End Sub 
 Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As  System.EventArgs) Handles Button3.Click      Me.BindingContext(DataView1).CancelCurrentEdit()      ComboBox1.Enabled = True      TextBox1.ReadOnly = True      Button3.Enabled = False      Button2.Enabled = True      DsCust1.Clear()      ' Restore previous record      DsCust1.Merge(mWebSvc.GetCustomer(ComboBox1.SelectedValue))    End Sub 

If the user cancels Add mode, we restore the last record that was displayed. Now code the most complicated part of the program. We must modify the Update button's click event to do a couple of things. First, if a record was added, we want it to refresh the dropdown list so we can see the new record. Next, if the user changes the company name field, we also want to update the list to reflect the new name . This sounds simple, and it is, but it adds some conditions to the code that prove quite interesting and some we haven't even seen yet. Here is the code:

 Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As  System.EventArgs) Handles Button1.Click      Dim dsTemp As localhost.dsCust      Dim strCust As String      Dim RePopulate As Boolean      strCust = TextBox1.Text      Me.BindingContext(DataView1).EndCurrentEdit()      ' In our case it will be either/or, since we are only dealing with  a one row datatable.      If DsCust1.HasChanges(DataRowState.Modified Or DataRowState.Added)  Then         dsTemp = DsCust1.GetChanges(DataRowState.Modified Or  DataRowState.Added)         ' Must test if adding a row or changed company name before we do  the update.        If dsTemp.Customers.Rows(0).RowState = DataRowState.Added Then  RePopulate = True        ' Since we are testing equality of two objects' VALUES we use the  "Is" operator.        ' The "Equals" method tests to see if two variables point to the  SAME object.        ' If they were value types we could use "=" but these methods  return objects.        If Not (dsTemp.Customers.Rows(0)("CompanyName",  DataRowVersion.Current) Is _             dsTemp.Customers.Rows(0)("CompanyName",  DataRowVersion.Original)) Then           RePopulate = True         End If         DsCust1.Merge(mWebSvc.UpdateCustomer1(dsTemp))         ' Only want to repopulate if added new row or changed Company  Name.         If RePopulate Then           PopulateList()           ComboBox1.SelectedValue = strCust         End If     End If     ComboBox1.Enabled = True     TextBox1.ReadOnly = True     Button3.Enabled = False     Button2.Enabled = True  End Sub 

I commented this code liberally, unlike my usual bad habit of not commenting at all. The trick is to know when I am adding a record, or changing the Company Name field. If either of these conditions is met, update the dropdown list, or update the database normally. The problem is that as soon as I update the database I lose visibility into what changed and what was added. So I use a Boolean variable to hold the condition, then I update the database, then I test the Boolean, and, if it is true, I refresh the list.

The way we test the conditions is interesting. To test to see if the row was newly added, I use the RowState property of the DataRow. This returns a DataRowState enumeration value that determines if the row was added, modified, and so forth. Next, to find out if the user modified the Company Name field, we use another form of the Item method of the DataRow. We can get any of four possible values for any given column: Current, Original, Proposed, and Default values. Normally there is only a Current value. If the user has edited the field, or if it was changed by assignment, and EndCurrentEdit has not been called, then there are Original and Proposed values. Once EndCurrentEdit had been called, but the row had not yet been updated to the database, there are Current and Original values. Whether or not there is a default value depends on how the field was defined. Since we have already called the EndCurrentEdit method, we will only have Current and Original values. We can then compare these values for any field to determine if it has changed. If we run the program, we can now change and add rows to the database. Just make sure you use a unique company ID value.

Master-Detail Data

We looked at master-detail data in the chapter on Windows Forms. We can add more code to support a DataGrid control without too much trouble. This time we do need to add a new web method to our Web Service. We need a method to update the Orders and Order Details tables. This will also demonstrate what we must do if we make a change to a Web Service. Add the following web method to the Web Service:

 <WebMethod(Description:="Updates Customer Order Info.")> _    Public Function UpdateOrders(ByVal dsOrders1 As dsOrders) As dsOrders      Try        dsOrders1.Errors.Rows.Clear()        If dsOrders1.HasChanges Then          SqlDataAdapter3.Update(dsOrders1.Orders)          SqlDataAdapter4.Update(dsOrders1.Order_Details)        End If      Catch errobj As Exception        dsOrders1.Errors.Rows.Add(New String() {errobj.Message,  errobj.StackTrace})      End Try      Return dsOrders1    End Function 

After we compile the Web Service, we must refresh the web reference in the client form. This is because we changed the contract, and the client needs to know. It must regenerate its proxy class to include the new web method.

This brings up an interesting point about Web Services. We need to understand more than ever the concept of the WSDL document and interface contract. We had a similar issue with COM objects and interfaces. Once we have published a Web Service, it is very important not to change any existing web method. This is because your service may be in use by any number of users, and if you change the parameters of a web method, you will break hundreds of applications that rely on your service. This can result in angry customer service calls if they are paying for using the Web Service. If you need to modify a service, you can create one that has the new functionality and instruct your users to use the new service if they need the new functionality. This way you don't break existing applications.

Next we add our grid control to the form. In order to do this, we have to rearrange the controls so we get a good fit. Figure 10.21 shows the form with the DataGrid on it.

Figure 10.21. The form with order information.

graphics/10fig21.jpg

I used the AutoConfigure option of the grid control and chose Professional3. I then set the BorderStyle property to Fixed3D. I also enclosed the three buttons in a panel and then docked the panel to the bottom of the form. You also need to add another DataSet. Select Localhost.dsOrders when the DataSet configuration dialog opens. Set the DataSource property of the grid to DsOrders1 and the DataMember property to Orders. Since there is a relation from Orders to Order Details, the grid will automatically be configured as a master-detail grid.

Now we need the code to populate the grid. As it turns out, this is quite simple. We need to add two lines of code to the ComboBox's SelectedItemChanged event:

 Private Sub ComboBox1_SelectedIndexChanged(ByVal sender As  System.Object, ByVal e As System.EventArgs) Handles  ComboBox1.SelectedIndexChanged      DsCust1.Clear()      DsCust1.Merge(mWebSvc.GetCustomer(ComboBox1.SelectedValue))      DsOrders1.Clear()      DsOrders1.Merge(mWebSvc.GetOrders(ComboBox1.SelectedValue))    End Sub 

You can test the application now if you like. Notice that it takes a lot longer for the form to start. This is because we now have to get all of the order and order detail rows each time we change items in the drop down. This first time is a little slow, but once the program is running, it is very quick. Here is where you need to plan carefully if you know you will be using a slow network connection. You may not want to download the entire set of orders and details to the client at once.

Next we need to modify the Update button's click event to handle updating the order information. We only want to do this if needed, and we only want to pass changes back to the Web Service to conserve bandwidth in case of the slow connection. The new click event for the Update button is:

 Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As  System.EventArgs) Handles Button1.Click      Dim dsTemp As localhost.dsCust      Dim dsTemp1 As localhost.dsOrders      Dim strCust As String      Dim RePopulate As Boolean      strCust = TextBox1.Text      Me.BindingContext(DataView1).EndCurrentEdit()      ' In our case it will be either/or, since we are only dealing with  a one row datatable.      If DsCust1.HasChanges(DataRowState.Modified Or DataRowState.Added)  Then        dsTemp = DsCust1.GetChanges(DataRowState.Modified Or  DataRowState.Added)        ' Must test if adding a row or changed company name before we do  the update.        If dsTemp.Customers.Rows(0).RowState = DataRowState.Added Then  RePopulate = True        ' Since we are testing equality of two objects' VALUES we use the  "Is" operator.        ' The "Equals" method tests to see if two variables point to the  SAME object.        ' If they were value types we could use "=" but these methods  return objects.        If Not (dsTemp.Customers.Rows(0)("CompanyName",  DataRowVersion.Current) Is _          dsTemp.Customers.Rows(0)("CompanyName",  DataRowVersion.Original)) Then          RePopulate = True        End If        DsCust1.Merge(mWebSvc.UpdateCustomer1(dsTemp))        ' Only want to repopulate if added new row or changed Company  Name.        If RePopulate Then          PopulateList()          ComboBox1.SelectedValue = strCust        End If      End If  Me.BindingContext(DsOrders1).EndCurrentEdit()   If DsOrders1.HasChanges(DataRowState.Modified Or DataRowState.Added   Or DataRowState.Deleted) Then   dsTemp1 = DsOrders1.GetChanges(DataRowState.Modified Or   DataRowState.Added Or DataRowState.Deleted)   DsOrders1.Merge(mWebSvc.UpdateOrders(dsTemp1))   End If  ComboBox1.Enabled = True      TextBox1.ReadOnly = True      Button3.Enabled = False      Button2.Enabled = True    End Sub 

The new code is bolded and in italics. If you test the code, you can make changes to the Orders grid, and when you click Update, the changes are sent back to the Web Service, which updates the database.

Next, we take a quick look at Web Forms, and then, that's it! You're a Web Services expert!


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