White Papers                  Home |  White Papers  |  Message Board |  Search |  Products |  Purchase | News |  

 

Passing data over .Net Web Services

 

by Rick Strahl

West Wind Technologies

http://www.west-wind.com/

 

 

Updated: 12/7/2001

 

Code for this article:

http://www.west-wind.com/presentations/dotnetwebservices/dotnetwebservices.zip

 

 

Amazon Honor System Click Here to Pay Learn More

 

 

 

 

 

 

Web Services are powerful technology even in its basic form. However, with .Net you can easily couple them with .Net's new data services to provide a powerful data delivery mechanism that works over the Web making it possible to build distributed applications that work easily without a local data store. In this article Rick describes various ways how you can use Web Services and ADO.Net DataSets to pass data between client and server applications to build truly disconnected apps.

 

Web Services are without a doubt the most talked about topic in .Net. I've introduced you to the basics of Web Services along with a number of new concepts of how you can pass different kinds of data over the wire a couple of issues ago. In this article I'll dig in on one aspect of using .Net Web Services as a transport for data allowing you in essence to build a remote data service with relatively little code. I'll show a simple example that demonstrates using ADO.Net DataSets for creating a remote data connection for a Windows Form based application.

 

.Net makes this type of functionality relatively easy by providing both the Web Service transport mechanism (SOAP and HTTP) as well the built in serialization services in the form of XML for all of the data objects it provides. Using ADO.Net DataSets makes this process very easy and allows you to easily create fully disconnected database applications that can access data over the Web using standard HTTP. In this article I'll focus on the ADO.Net DataSet object as the data transfer object that is passed back and forth.

 

Dataset objects in .Net

DataSets – blurring the line between data and XML

.Net introduces ADO.Net as the primary data access mechanism to access data in traditional database tables as well as other non-relation data sources. As ADO did prior to .Net, ADO.Net aims to abstract data to the point where the backend that is accessed doesn't matter. ADO.Net provides the front end view to that data. ADO.Net is a monumental improvement over the functionality that ADO provided in that it provides a much cleaner object model and much more flexibility in how data is represented and how it can be manipulated. It allows the ability to create an offline data view of multiple tables or data sources into a single object that contains both the data itself and a bunch of meta data that describes the data contained within the data set. In addition datasets can directly create data tables (roughly the equivalent of an ADO recordset) from XML input without any additional conversion routines. ADO.Net uses XML as its native data format and the data stored in datasets is internally represented as XML. What this means is that you can easily pass data from non .Net sources to a .Net application and have a dataset accept that data without ever having to manually parse that data through an XMLDOM, SAX or XML Text Reader type parser. All of this comes for free as part of the .Net XML support and it really doesn't ring home until you have occasion to take advantage of this functionality.

 

Datasets also intrinsicly have the ability to take data offline. In fact, Datasets are always offline views of data that is not connected to the datasource. What this means is that you can pass the data around to other applications, make changes to it, then update the original data source with the changes. The Dataset manages the changes made to the data and can re-synch with the database as needed. This makes it possible to use DataSets fairly transparently as either a remote data source, or in an offline application. DataSets can both handle single record update functionality (ie. The request/response model) or the fully offline scenario where the data is pulled down as big chunk, worked on and modified and then eventually re-synchronized with the data source.

 

 

Figure 1 – Remote data access with .Net is easy by using Web Services and DataSets used as parameters. .Net can handle all the details of persisting and restoring a full DataSet object over a Web Service including the ability to merge changes into the database providing a powerful tool for building remote data services over the Web.

Web Services and Data

As you might expect, Web Services provide the perfect data delivery mechanism for using these disconnected datasets. Let's look of a simple example that once again uses the Pubs SQL Server sample database. What I'm going to do is create a simple Windows form that allows retrieving author records and edit them,then send the edited data back up to the server for synchronization. I'm going to present two separate scenarios here:

 

  • Connected
    In this scenario I assume the client has a constant connection to the Web and is pulling data in small chunks. This is not 'connected' in the sense that we have direct connection to the database, but connected to the Web and accessing data through an always on Web Service. We'll retrieve a list of Authors and then pull each author down individually and update the database with each Save operation.

 

  • Disconnected
    In this scenario all of the data is pulled down to the client side with the assumption that the client is disconnected from the Internet while working on the data. Since the full data set was returned the client can work on multiple records while disconnected and change the data and finally re-connect to the database to sync the changes back up to the database over the Web when a connection is available

 

Faster access to SQL Server using the SqlClient data library

What's interesting with these two scenarios using Web Services and ADO.Net DataSets is that the code for these two implementations is almost identical.

 

I'm not going to go through all the steps of creating a new Web Service project – if you're new to Web Services see my previous article (in Issue 1, 2001 or at http://www.west-wind.com/presentations/dotnetwebservices/DotNetWebServices.asp). I'll add the following classes and examples to the project previously discussed there. You can find the code online at the URL listed above. I'm also using a slightly modified version of the Pubs database – I added a pk field to the Authors table, since my applications tend to rely on integer keys and I forgot to use the Id field instead. Add the field as an integer and make it an Identity column so it gets auto-assigned values. In this example, I also use the SqlClient class as opposed to the OleDb client to access data (see sidebar).

 

 

Let's start by adding a new Web Service called ComplexTypes and adding a few methods to it. This time around I'll use a few generic routines to pull data from the datasource in a more simplified manner.  I'll introduce these as we go along. Before I show you the code for the Web Service let me throw two helper methods used for querying data from a SQL Server database into a DataSet to simplify data access.

 

In these examples, we use two protected helper methods: Open() which opens a connection and Execute() which runs a SQL statement that returns a result set. These make accessing data easier – for real applications you should probably uses separate data access object to perform these type of tasks. For simplicity's sake I include them here as part of the Web Service class as non-exposed methods. It also requires a few extra additional namespace and properties on class itself:

 

/// Add these namespace reference to the Web Service

using System.Data;

using System.Data.SqlClient;

 

/// Add these property definitions

/// Make sure you adjust the connection string as necessary

public string cConnectString =   "server=(local);database=pubs;uid=sa;pwd=;";

public SqlConnection oConn   =   null;

public DataSet   oDS         =   null;

public string cErrormsg      =   "";

public bool lError           =   false;

 

These properties are used to deal with the SQL Server connection used by the following two methods:

 

protected bool Open()  {

 

if (this.oConn == null) {

   try {

      this.oConn = new SqlConnection(this.cConnectString);

   }

   catch(Exception e)    {

      this.SetError( e.Message );

      return false;

   }

}

 

if (this.oConn.State != ConnectionState.Open)  {

   try {

      oConn.Open();

   }

   catch(Exception e) {

      this.SetError( e.Message );

      return false;

   }

}

 

/// make sure our dataset object exists

if (oDS == null)

   oDS = new DataSet();

 

return true;

}

 

protected int Execute(string lcSQL, string lcCursor) {

 

if (!this.Open())

  return 0;

 

SqlDataAdapter oDSCommand = new SqlDataAdapter();

oDSCommand.SelectCommand =

   new SqlCommand(lcSQL,oConn);

 

/// remove the table if it exists

try

{

   oDS.Tables.Remove(lcCursor);

}

catch(Exception e) { }  // Ignore the error if no table

/// Create a DataSet/DataTable object from the

try  {

   oDSCommand.Fill(oDS,lcCursor);

}

catch(Exception e)  {

   this.SetError(e.Message);

   return 0;

}

 

return oDS.Tables[lcCursor].Rows.Count;

}

 

Execute returns a count of rows returned from the current query. It also sets the cErrormsg and lError flags if an error occurs so you your client code doesn't have to worry about trapping for execution failures by simply checking the lError flag for operational errors.

 

Ok, back to our Web Service. Let's start by creating a GetAuthorList() method in the Web Service which retrieves only a list of Authors – it pulls only a PK and the display fields to be used in a list display. Now that we can easily generate a DataSet object let's create the method which runs a query of authors and returns a DataSet containing a table that holds only a display field (to be used in listboxes or combos) and a PK.

 

[WebMethod]

public DataSet GetAuthorList(string lcNameFilter)  {

 

    if (lcNameFilter.Length == 0)

      lcNameFilter = "%";

 

   /// Open the connection to the database

   if ( !this.Open() )

      return null;

 

   int lnResult = this.Execute("select au_lname+ ', ' +

                               au_fname AS name, pk " +

                               "from authors where au_lname like '" +

                               lcNameFilter + "%'" +

                               " order by au_lname","Authors");

   if (this.lError)  {

      this.Close();

      return null;

   }

 

   return this.oDS;

}

 

 

Note that the method returns a DataSet as a result of the query. The DataSet will be marshalled back to the client calling the Web Service so the client can pick up the object with the same data and object attributes as the DataSet passed down from the server.

 

To consume the Web Service on the client side we have to (again, you can look at the previous article if this is new to you):

 

  1. Add a Web Reference to the ComplexTypes.asmx page
  2. Rename the reference to TypeWebService from localhost or whatever server name you used
  3. Create a new Windows Form called AuthorForm (see Figure 2)
  4. Add the TypeWebService namespace (using CodeClient.TypeWebService)
  5. Add a listbox control called oAuthorList to the form
  6. Add the following code

 

Figure 2 – The 'connected' author Windows form in the Visual Studio editor. The controls of this form will be bound to data retrieved from the Web Service.

 

Create the following method to pull the data from the Web Service and populate the listbox:

 

private void GetAuthorList()

{

   ComplexTypes oService = new ComplexTypes();

 

   try  {

      oDS = oService.GetAuthorList("%");

   }

   catch(Exception e)  {

      MessageBox.Show(e.Message);

      return;

   }

 

   /// Direct data binding code

   oAuthorList.DataSource = oDS.Tables["Authors"];

   oAuthorList.DisplayMember = "name";

   oAuthorList.ValueMember = "pk";

}

 

To call this method when the form loads use the form's Constructor (AuthorForm()) to add the following:

 

oDS = new DataSet();

oAuthorDS = new DataSet();

 

GetAuthorList();

 

NameSpaces for Web Services

The first two commands set properties I added to the form that will contain persistent references to the DataSets returned from the Web Service. The GetAuthorList() method sets the oDS member with the Authorlist result – we'll use the oAuthorDS later to pull down the individual Author data.

 

 

Notice how little code is involved with pulling this data down from the server. We simply create a reference to the Web Service and retrieve the DataSet from the GetAuthorList method of the ComplexTypes Web Service. Once on the client this DataSet object is fully functional and fully disconnected from the database.

 

For error handling purposes any errors from the server are marshalled down to the client via the SOAP method and the try/catch block deals with catching any errors and displaying an error message. The Exception object contains detailed information from the error fired on the server or if a connection couldn't be made to the Web Service.

 

To bind the data to the listbox this time around I use .Net's databinding features by assigning a datasource (a table from the DataSet) and setting the Display and ValueMember properties with the appropriate field names to display.

 

What's happening here is that I'm simply pulling a display only list down with only the data I want to see in the listbox, plus the PK so I can ask for the individual author data from the Web Service when I navigate to a new record in the list. So, the next step is to actually create a Web Service method that can return me a single Author record from the table. Here's that Web Service code added to ComplexTypes.asmx:
 

 

 

 

 

 

 

 

 

[WebMethod]

public DataSet GetAuthor(int lnPK) {

if ( !this.Open() )

   return null;

 

int lnResult = this.Execute("select * from authors " +

    "where pk=" + lnPK.ToString(),

    "Author");

 

this.Close();

 

if (this.lError or lnResult = 0) {

   return null;

}

 

return this.oDS;   }

 

Simple enough. Same rules as before – to pick up the record on the client side in the form I add another method to the form to load the Author and bind the dataset's fields to the form controls shown in figure 2 (you can see the field names below):

 

public void BindData() {

 

/// Get the index of the item selected

int lnIndex = oAuthorList.SelectedIndex;

 

/// grab the data row

DataRow loRow = oDS.Tables["Authors"].Rows[lnIndex];

 

ComplexTypes oService = new ComplexTypes();

 

/// Call the Web Service with the PK

try  {

   oAuthorDS = oService.GetAuthor( (int)loRow["pk"] );

}

catch(Exception ex) {

   MessageBox.Show(ex.Message);

   return;

}

 

if (oAuthorDS == null) {

      MessageBox.Show("Couldn't load customer");

      return;

}

  

/// This code uses control data binding to the Data Set's fields

txtLastName.DataBindings.Clear();

txtLastName.DataBindings.Add(new Binding("Text",oAuthorDS.Tables["Author"],"au_lname"));

 

txtFirstName.DataBindings.Clear();

txtFirstName.DataBindings.Add(new Binding("Text",oAuthorDS.Tables["Author"],"au_fname"));

 

txtAddress.DataBindings.Clear();

txtAddress.DataBindings.Add(new Binding("Text",oAuthorDS.Tables["Author"],"address"));

 

txtPhone.DataBindings.Clear();

txtPhone.DataBindings.Add(new Binding("Text",oAuthorDS.Tables["Author"],"phone"));

 

chkContract.DataBindings.Clear();

chkContract.DataBindings.Add(new Binding("Checked",oAuthorDS.Tables["Author"],"contract"));

}

 

Once again the data is retrieved as a DataSet from the Web Service this time using the GetAuthor() method which is passed the primary key retrieved from the list box. To figure out which PK to load we can use the list box index which maps the dataset row that contains the record in question. Note that both the list box index and the items DataTable's rows are 0 based indexes (you can also use the controls SelectedValue property but I had problems with this as the databinding caused this method to fire before data was present in the list resulting in type casting errors).

 

It's important to note though that the binding is one-way in this scenario. Although .Net supports databinding to controls and navigation through the DataSet using the form's BindingContext property (e.g.:  this.BindingContext[oAuthorDS,"Author"].Position = 0; which refreshes the data set to the row specified) it doesn't work here because we're actually pulling down a fresh DataSet everytime we use the Web Service to get an Author.  So if the value of data underneath changes (like the record pointer moves), we have to rebind to the data. Binding however does bind to the actual record object data so when a change is made to the textboxes or checkbox it reflects right back into the DataSet.

 

Take a look at how the databinding code works – not the most intuitive way to bind to data. But once you've done the binding any changes made to the control automatically map to the underlying datasource which in this case are all of the fields. You can update the field's value and the datasource is updated with the changed data. Notice that you can bind anything to anything! For example, look at the checkbox which binds the data to the Checked property rather than Value. I could also bind to the Text property, which would in turn change the label of the checkbox to True or False.

 

Figure 3 – the running for pulls its data from the Web Service and binds to the resulting DataSet. Each Author is pulled when the user requests it thereby minimizing the data travelling over the wire.

 

DataSet and Updates

So, the databinding can now take care of updating our local DataSet for us. In this scenario so far we've pulled list of authors for the listbox into one dataset and we pull one author at a time into another dataset as we click on the list box, binding the fields to the various controls on the form. The update then fires from:

 

private void oAuthorList_SelectedIndexChanged(object sender, System.EventArgs e) {

BindData();

}

 

As you make a change to the entry fields the changes are written back into the DataSet automatically. When you click Save on the form we now want to take the changed data back up to the server. So the idea is we're offline while editing, but we write the data back into the database immediately after making that change.

 

To do this we need to add another method to our Web Service called SaveAuthor() which receives a DataSet as a parameter. We'll pass the changed DataSet up to the server through the Web method call. Here's the code for the SaveAuthor() Web method:

 

[WebMethod]

public bool SaveAuthor(DataSet loClientDS)

{

   if (!this.Open())  

      return true;

 

   /// Even though we won't execute this command the data adapter needs this

   /// command to figure out the key field info in order to properly do updates

   SqlDataAdapter oDSCommand = new SqlDataAdapter("select * from authors",this.oConn);

 

   /// This builds the Update/InsertCommands for the data adapter

   SqlCommandBuilder oCmdBuilder = new SqlCommandBuilder(oDSCommand);

 

   int lnRows = 0;

   try

   {

      /// Take the changes in the dataset and sync to the database

      lnRows = oDSCommand.Update(loClientDS,"Author");

   }

   catch(Exception e)

   {

      return false;

   }

 

   if ( lnRows < 1 )

      return false;

 

   return true;

}

 

This deceptively simple code retrieves the DataSet passed from the client side and saves it into the database. It takes very little code to do the update process here because the DataSet can automatically detect changes to its content and update the backend from the changes. There's one trick to this process – and it took me a while to figure this out because the .Net docs do not explicitly discuss how to reconnect a disconnected data set.

 

The first thing that needs to happen is that we need to specify a SQL statement from the backend that was used to create the data set on the client side. This is used so the DataSet can get key field information from the SQL Server. Note that the select statement is not actually executed in full, but rather used to get the schema information about the table used in the query and the relationships if any. These are neeeded in order to merge changes back into the database. Note that this functionality only works if the database tables contain primary keys! If you don't have primary keys you will have to manually provide key and statement information.

 

Another important piece is the SqlCommandBuilder object which is responsible for building SQL Update and Insert statements for the DataAdapter used to talk to the database. Think of the DataAdapter of the interface that connects the disconnected DataSet with the database. The DataAdapter is the one issuing the actual SQL commands and talking to the backend database using a Connection object to connect to the server.

 

For our update process here in particular the key property of the DataAdapter is the UpdateCommand which an object create SqlCommand object normally – this allows you to create a custom UPDATE or INSERT or DELETE statement for when update operations need to fire for a particular table to be synched up with the server. You can manually do this with code like this:

 

oDSCommand.UpdateCommand = new SqlCommand("UPDATE authors SET au_lname='somevalue',au_fname=... where PK=" + lnPk.ToString(),oConn);

 

In other words you provide a physical SQL command to perform the update operation yourself. That seems like a lot of work though especially since you have to do the same for inserts and delete operations.

 

To make life easier the SqlCommandBuilder can be used to automatically generate those statements for you by passing in a data adapter as a parameter to the constructure.

 

SqlCommandBuilder oCmdBuilder = new SqlCommandBuilder(oDSCommand);

 

To be honest I'm not quite clear what happens when you do this, because if you check the various Command properties on the DataAdapter or the CommandBuilder nothing seems to be set, but rest assured when the update is performed it works and auto-generates the right update command for the data in question. Without the SqlCommandBuilder it would fail. The actual update is a single line of code:

 

lnRows = oDSCommand.Update(loClientDS,"Author");

 

You call the DataAdapter with the DataSet and specify which table it should merge. If you have multiple tables that need updating you can call Update() multiple times for each one of the DataTable objects contained in the DataSet.

 

Now of course I'm simplifying somewhat. You may have to deal with conflict resolution in this scenario, so if the data has changed between the time it was offline and reconnects you can get errors thrown by the update process. The DataSet actually provides a number of properties that deal with conflict resolution from tags that tell you which updates didn't take and why. But that's a topic for a future DataSet specific article… let's keep the focus on the data transfer using Web Services for now.

 

If you think about how much functionality is wrapped up into a fairly easy to manage mechanism it's easy to get excited about the possibilities. Using this mechanism you can create local client applications on remote workstations that use the Web as the network to access remote data served from the Web server using DataSets. And the beauty of it is that it's a standard format that's travelling ove the wire in the form of a DataSet – you manipulate the DataSet the same way on both the client and the server using the offline paradigm for the DataSet itself.

One more step – totally disconnected data

But wait, there's more. As you probably already realized it's possible to take the data offline in a more permanent scenario. In the previous example we need to have a live Web connection in order to get the Author data to the client. But what if we're not connected?

 

Figure 4 – The disconnected form can go offline after it's retrieved the data from the server. You can make changes to the data and then update the data to the server when you get back online allowing full offline operation.

 

Since DataSets are disconnected data containers, it's just as easy to make changes to a whole bunch of records as it is to the single record as just demonstrated. And the nice thing is your code requires very few changes to make this happen. In fact I just copied the form on the client side and added a couple more methods to the Web Service.

 

[WebMethod]

public DataSet GetAuthors(string lcNameFilter)

{

 

if (lcNameFilter.Length == 0)

   lcNameFilter = "%";

 

if ( !this.Open() )

   return null;

 

int lnResult = this.Execute("select * from authors where au_lname like '" + lcNameFilter + "%'" +

   " order by au_lname","Authors");

 

if (this.lError)  {

   this.Close();

   return null;

}

 

return this.oDS;

}

 

[WebMethod]

public bool SaveAuthors(DataSet loClientDS)

{

if (!this.Open())  

   return true;

 

SqlDataAdapter oDSCommand = new SqlDataAdapter("select * from authors",this.oConn);

 

SqlCommandBuilder oCmdBuilder = new SqlCommandBuilder(oDSCommand);

 

int lnRows = 0;

try  {

   lnRows = oDSCommand.Update(loClientDS,"Authors");

}

catch(Exception e)

{

   return false;

}

 

if ( lnRows < 1 )

   return false;

 

return true;

}

 

Look at that! The code is almost identical! All that's really changed is the SQL Statement in GetAuthors() and the name of the table in SaveAuthors(). So, we're returning all records at once now to the client so that the client can go offline with the data and work with it locally before re-synching to the server.

 

On the client side I created a new form called AuthorFormOffline by simply copying the original form and renaming the class and several of the methods (the Constructor and AuthorForm_Load) to reflect the new name.

 

private void GetAuthors(bool llRefresh)

{

if (llRefresh)

{

   ComplexTypes oService = new ComplexTypes();

 

   try   {

      oDS = oService.GetAuthors("%");

   }

   catch(Exception e)  {

      MessageBox.Show(e.Message);

      return;

   }

}

 

oAuthorList.Items.Clear();

 

DataTable oTable = oDS.Tables["Authors"];

for (int x=0;x < oTable.Rows.Count; x++)  {

   oAuthorList.Items.Add(oTable.Rows[x]["au_lname"].ToString().Trim() +", " +

      oTable.Rows[x]["au_fname"].ToString());

}

 

}

 

This code is a little different in that this time around I'm manually populating the oAuthorlist ListBox. This is because I ran a select * on the server and did not include a display field in the SQL to bind the listbox to, so I have to manually populate the list. I also included a flag to refresh the list from the data so that if the data changes the list can be refreshed easily by repopulating the list from the local DataSet as opposed to re-fetching the data from the server.

 

There's also another overloaded method GetAuthors() method that takes no parameters which has llRefresh set to true:

private void GetAuthors()  

{    GetAuthors(true);  }

 

This is because C# does not support default parameters of any type so the only way to get two different parameter signatures is to overload the method by creating a new method, which now behaves the same way as the GetAuthors did in the original AuthorForm.

 

Now when we click on the form we'll want to display the data again in the fields of the form. To do this I'll use manual binding just for kicks so you can see how that would be done:

 

public void BindData()   {

   nIndex = oAuthorList.SelectedIndex;

  

   DataRow loRow = oDS.Tables["Authors"].Rows[nIndex];

 

   /// Now manually bind the data to the data row - so updates

   /// go straight back to the DataTable in the DataSet

   txtLastName.Text = loRow["au_lname"].ToString();

   txtFirstName.Text = loRow["au_fname"].ToString();

   txtAddress.Text = loRow["address"].ToString();

   txtPhone.Text = loRow["phone"].ToString();

   chkContract.Checked = ((bool) loRow["contract"]) ? true : false;

}

 

Note we simply reference the row in the DataSet that exists on the client now rather than refetching the data from the server. This is of course very fast since the data is local.

 

The data is then updated, but remember we didn't bind the data to the form controls so when we click on the Save button we have to manually update the DataSet from the form:

 

public void SaveAuthor()

{

   /// Update local DataSet only

   DataRow loRow = oDS.Tables["Authors"].Rows[nIndex];

 

   loRow["au_lname"] = txtLastName.Text;

   loRow["au_fname"] = txtFirstName.Text;

   loRow["address"] = txtAddress.Text;

   loRow["phone"] = txtPhone.Text;

   if (chkContract.Checked)

      loRow["Contract"] = true;

   else

      loRow["Contract"] = false;

 

   /// Refresh the list but don't refetch the data

   GetAuthors(false); 

 

   /// reselect item in the list

   oAuthorList.SelectedIndex = nIndex;   

}

 

The Save operation saves to the DataSet locally and again does not access data externally. All the changes are made to the local DataSet which keeps track of the changes made internally.

 

Finally when we do connect back to the Internet we'll want to merge the changes made offline back to the server and we do this in exactly the same was we did before when we sent a single DataSet record up to the server. The difference now is that there may be more than one record to update.

 

public void UpdateAuthors()

{

   /// This should show all the changed records in the record set

   // MessageBox.Show(ShowDS(oDS.GetChanges()));

 

   ComplexTypes oService = new ComplexTypes();

 

   bool llResult = false;

 

   try

   {

      llResult = oService.SaveAuthors( oDS );

   }

   catch(Exception ex)

   {

      MessageBox.Show(ex.Message);

      return;

   }

 

   if (llResult)

      MessageBox.Show("Authors Saved");

   else

      MessageBox.Show("Authors were not saved");

 

   GetAuthors();

 

}

 

That's it! This code on the client side is just about identical to the single record version and the server side too doesn't need to do anything differently.

 

To see what data actually gets sent to the server, uncomment the MessageBox call above – I created a method called ShowDS to display the content of a DataSet in XML form at any time. This useful debugging method looks like this:

 

public string ShowDS(DataSet loDS)

{

   System.IO.StringWriter loStream = new System.IO.StringWriter();

   loDS.WriteXml(loStream);

 

   return loStream.ToString();

}

 

When you run this on the changes to the dataset you'll see each of the records that was changed shown there which gives you a pretty good idea on how the DataSet/DataAdapter resolve changes.

 

Security

Ah yes, the good old topic of security invariably will come up when you're passing data over the wire in this fashion. There are a couple of issues to deal with:

 

  • Data on the wire
  • Access to the server

 

Fortunately, Web Services can take advantage of features in HTTP to help with these issues natively or if you choose you can implement your own mechanisms.

HTTPS

To make sure that data on the wire is secure you can use the HTTPS protocol. To make sure HTTPS/SSL is used simply change the URL to the Web Service to an HTTPS:// protocol link. This assumes the server has an SSL certificate installed. Once you call the Web Service with an https:// link both the WSDL content and the actual Web Service data will go over the wire securely.

Application Security

Securing access to an application can be accomplished in a number of ways using standard Internet security with Basic Authentication or NT File security (NTLM) or by using a custom scheme. There's really little reason to use Basic Authentication unless you want to allow selective access to some methods and not others in which case your code can decide when to authenticate and when not to.

 

My recommendation for a Web Service is to use NTLM file security. For example, if I wanted to lock the Web Service down to only be accessible by Adminstrative users go into Explorer on the Server, find our ComplexTypes Web Service on disk and remove all the security access except for the Administrators group from the ComplexTypes.asmx. Apply the changes and just for kicks run the Web Service client apps now – you'll find that all remote calls will immediately fail with an Access Denied or Unauthorized error message. The reason for this of course is that we haven't told the Web Service who we are yet.

 

To pass the user context to the Web Service you can use the Credentials member of the client side Web Service proxy. The following sets the Credentials to the currently logged on user on the client machine:

 

ComplexTypes oService = new ComplexTypes();

oService.Credentials = CredentialCache.DefaultCredentials;

 

try {

oDS = oService.GetAuthorList("%");

}

catch (Exception ex) ( ; )

 

As you can see having the error handler around the Web method call is fairly crucial in order for you to trap a possible logon failure as well as connection issues or outright failures on the Web Service on the server.

 

CredentialCache is a CONST object that is always available if you include the System.Net namespace. It provides information about the current user including his or her security context and SID (if you've ever worked with some of the Win32 Security APIs you know this is a big time saver).  If you want to specify a user account manually (you might decide to create a special account to allow access to the Web Service) you can do so like this:

 

oService.Credentials = new NetworkCredential("username", "password","domain");

 

The domain parameter is optional if you're using a local machine account, but remember that the account you're using must be something that both the Web Server and your local machine can validate against.

 

Finally if you choose to you can also use application specific security by having parameters passed for usernames and passwords as part of some Web Service methods. For example, you could implement a Logon method that performs you user check and then then use the ASP.Net Session object to persist the user's login so you don't have to keep checking. The WebService proxy class wrapper includes support for Cookies. Remember Web Services are just wrappered ASP.Net applications so you have access to all of the standard Web Context objects to retrieve info including cookies and session variables.

Remoting the kitchen sink

I hope this article has given you some ideas on how you can use Web Services with data. There have been a lot of different schemes available over the last few years to pass data over the wire including Microsoft's RDS, pure XML transfers and several XML data services such as the SQL Server's SQL HTTP Service. But I believe that DataSets are an important step in providing a consistent interface with an XML base that makes it possible to use a single data access mechanism to service many different types of usage scenarios. For example, because of the modular nature of DataSets they can often become a direct part of your business objects (replacing object members for row data for example) while providing most of the functionality a data layer usually needs to provide. All of this translates to a lot less code that needs to be written to handle data access especially if that data passes over the Internet. Combined with Web Services DataSets make it much easier to tie the business logic to the data access, because a Web Service can service data and also run code at the same time as part of a single request. This wasn't possible using RDS or SQL Server over IIS because those mechanisms hit the backend directly. It was possible with custom XML implementations but required a fair amount of code especially if you weren't using a framework that already supported XML conversions and HTTP access. .Net has also optimized the process of getting the data out of the database, into XML and persisting it over the wire through DataSets. So much so that performance and XML conversions are no longer a big issue for pure backend business applications as was the case with COM solutions in the past.

 

While all of this has been possible for a long time .Net reduces the amount of code that needs to be written to a very small amount because the framework deals with the semantics of pushing the data over the wire and serializing it. This makes for very powerful functionality that allows you to serve business logic to anywhere with little effort. Best of all you'll use consistent mechanism – the DataSet to manipulate the data both on the server and client, with clients that either are Windows Forms as I've shown here or other Web Services or Web pages (ASP.Net or otherwise) that consume these services.

 

On more personal note, when I started out putting this article together I wanted to write about several different mechanisms to move complex data over the wire, but in the course of putting the examples together and working on another project simultaneously I realized that there was way too much to cover just in the data service management on its own. While it is impressive to see how 'easy' things can be with .Net, it's not always so easy to arrive at these solutions. Writing this article and researching some of the related issues of security and data binding actually took several days when I expected it to take maybe one. Hitting a few dead ends here or there also didn't help. Finding help in the documentation usually was pretty fruitless, but the newsgroups and a large number of articles that are published online have been very helpful. If you do get stuck in your own work and learning be sure to take advantage of the Microsoft newsgroups (as well as other .Net hangouts like the DevX forums) as they are active and there's lots of material to read through and find. Lots of people are offerring help and are posting useful code for others to look at. And even though articles like these and many others in this and other magazines and online will help it's not always easy to find the right information you need when you need it even if it is there buried below 20 other magazines you haven't gotten around to read yet (Code not among them I trust, right?). Make sure you take advantage of the interactive resources available! It's important to the climb of the .Net curve…

 

As always if you have questions or comments you can post them at:

http://www.west-wind.com/wwthreads/default.asp?Forum=Code+Magazine

 

I'll have more for 'ya on Web Services in the next issue when I hope to focus on integrating Web Services and data services into business objects. Until then, don't work too hard…

 

Resources

 

Code for this article:

http://www.west-wind.com/presentations/dotnetwebservices/dotnetwebservices.zip

 

Introduction to .Net Web Services article:

http://www.west-wind.com/presentations/dotnetwebservices/DotNetWebServices.asp 

 

Microsoft Web Services Newsgroup:

news://msnews.microsoft.com/microsoft.public.dotnet.framework.aspnet.webservices

 

 

For comments, question etc. you can post a message at:

http://www.westwind.com/wwthreads/default.asp?forum=Code+Magazine.

 

Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development and tools with focus on Windows 2000, ISAPI, .Net and Visual Studio. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro and West Wind HTML Help Builder. He's also a Microsoft Most Valuable Professional, and a frequent contributor to magazines and books. He is co-publisher and co-editor of CoDe magazine, and his book, "Internet Applications with Visual FoxPro 6.0", is published by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/.

 

 

 

  White Papers                  Home |  White Papers  |  Message Board |  Search |  Products |  Purchase | News |