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

Using Visual FoxPro to call
.Net Web Services for Data Access



By Rick Strahl
www.west-wind.com
rstrahl@west-wind.com

 

 

Last Update:

February 14, 2009


 

Source Code for this article:
http://www.west-wind.com/presentations/FoxProNetWebServices/FoxProNetWebServices.zip

 

West Wind Web Service Proxy Generator Tool:
http://www.west-wind.com/wsdlgenerator/

 

 

If you find this article useful, consider making a small donation to show your support  for this Web site and its content.


 

 

Using Web Services from Visual FoxPro is not difficult, but dealing with Data or Complex objects is not quite as straightforward  as it could be. In this article, I’ll describe how you can work with .Net Web Services and pass complex data between VFP and .Net and handle updating scenarios for Data between the two.

 

Accessing Web Services from Visual FoxPro is nothing new. But effectively leveraging Web Services and interacting with them especially when dealing with .Net requires a more complete understanding of the mechanics and the tools available to Visual FoxPro developers, than merely creating the service and firing off method calls

 

Using the SOAP Toolkit from VFP is straight forward and calling .Net Services is no different than calling any other service. But things get sticky when we’re not running simple ‘Hello World’ type examples that return little more than simple types. The issues involved here deal with the fact that the SOAP Tooolkit (and most other tools SOAP Tools available to Visual FoxPro) are woefully underequipped to effectively pass and translate complex types. In this article

 

I’ll specifically look at passing DataSets and objects between Visual FoxPro and a .Net Web Service and demonstrate how the simple call model falls apart quickly when using complex types and datasets. It’s not difficult to work around these issues, but you as the Visual FoxPro developer has to realize that there will be extra work and likely some manual XML parsing to make this happen.

 

In this article I’ll keep things pretty simple from an application point of view to demonstrate the Infrastructure mechanics. I’m going to skip over some architectural issues such as business object usage and defer that to the simple business object that is included in the source

 

, but realize that these same principles also work with more complex data scenarios that involve multiple tables or hierarchical objects. To follow along with this article you’ll need Visual FoxPro 8 (or later), Microsoft .Net 1.1, Microsoft IIS and Sql Server or MSDE with the Pubs Sample Database installed.

 

Calling .Net Web Services from VFP

Let’s start with a quick review on how to create a .Net Web Service and call it from Visual FoxPro. I’m going to use Visual Studio to do this, but you can also do this without it, manually creating the project and virtual directories and compile your Web Service and support files. You can check the .Net framework documentation on how to do this. VS.Net makes this process much easier and so this is what we’ll use.

 

Start by creating a new Web Service project. I call mine FoxWebServiceInterop:

 

  1. Start the File | New | Project and select new Visual C# Sharp Project.
  2. Select Web Service and type in your project name and Web Server path. Figure 1 shows this dialog.

 

 

Figure 1 – The new project dialog creates a new VS.Net Solution and project and creates a virtual directory on your local IIS Web Server to host it.

 

The New Project Wizard then proceeds to create your solution and project by creating a virtual directory on your Web Server and copying a default web.config file and global.asax file which are application specific files that are required for any Web application. A .Net Web Service is really little more than a specialized Web application that uses an HTTP Handler to process Web Service requests. In fact if you later choose to add ASP. Net Web Forms to your project, they too can run out of this same directory.

 

The first thing I usually do to any ASP. Net or Web Service project is remove the WebService1.asmx page and instead create a new one with a proper name called FoxInteropService.asmx. In Figure 2 I also added a new folder to project and stuck a few support classes into it that I will use later on to handle data access. This is optional of course, but it’s a good idea to organize your projects as soon as you start adding files to the project.

 

Figure 2 – The new project created with the FoxInteropService.asmx Web Service file as well as a couple of supporting business object classes I’ll use later on

 

Get to the code

So now we’re ready to create our Web Service logic. Let’s start very simple with the mandatory Hello World example. Open the FoxInteropService.asmx page in Code View. What you will find is a class that was created for you with the same name as the service.

 

[WebService]

public class FoxInteropService : System.Web.Services.WebService

{

   … Component Initialization code omitted for brevity

}

 

The [WebService] attribute designates that this class will act as an endpoint for a Web Service and that any method that is marked up with the [WebMethod] attribute will accessible through the service. One of the things you probably will want to do right away is set a few additional options on the WebService attribute to tailor the Web Service for your application:

 

[WebService(Namespace="http://www.west-wind.com/FoxWebServiceInterop/",           

             Description="Small Sample Service that demonstrates interaction between .Net Web Services and Visual FoxPro")]

public class FoxInteropService : System.Web.Services.WebService

 

This sets a specific and hopefully unique namespace for your Web Service and provides a description which will be visible in the WSDL file for the Web Service as well as the Service Description test page. Remember that a namespace is a URI that is used as a unique identifier – it doesn’t have to be a valid URL or even a URL at all although by custom URI tend to point at something that looks like a URL. Use any unique value that can identify the service.

 

Ok, time to make the service do something,so let’s add add the following code (or modify the default commented out HelloWorld method):

 

[WebMethod(Description="The Quintessential Hello World Method")]

public string HelloWorld(string Name)

{

   return "Hello World, " + Name + "! The time is: " +

             DateTime.Now.ToString();

}

 

The Description parameter is optional, but again a good idea to provide the WSDL and Test Page with some information about your method – [WebMethod] by itself is valid.

 

Go ahead and compile the code. Assuming the code compiled you can now test the Web Service interactively by accessing the Web Service ASMX page through a browser with:

 

http://localhost/FoxWebServiceInterop/FoxInteropService.asmx

 

Figure 3 shows what the resulting HTML test interface looks like. This handy test page allows you to quickly see the methods available and for the most part lets you test the Web Service functionality through a simplified non-SOAP interface. Make sure that you can test the Service through this interface first to make sure everything’s working on the server.

 

Figure 3 – A .Net Web Service provides you with a nice Service Description page that shows all available methods and lets you test them (assuming the methods use simple parameters as input). Note that any descriptions you provide in the WebService attributes show up here.

 

And off we go to access the service from Visual FoxPro. The following code requires that you have the SOAP Toolkit Version 3.0 installed. From the command window or in a small PRG file you can type:

 

o = CREATEOBJECT("MSSoap.SoapClient30")

? o.MSSoapInit("http://localhost/FoxWebServiceInterop/" +             

               "FoxInteropService.asmx?WSDL")

? o.HelloWorld("Rick")

 

To which you’ll get the super exciting response:

 

Hello World, Rick! The time is: 4/8/2004 8:14:38 AM

 

And that’s all it takes for basic access of the Web Service. Note that you can make several method calls against the service after the initial MSSoapInit call, but you cannot call MSSoapInit more than once – for example to connect to a different Web Service. You’ll need a new instance of the SoapClient instead.

 

Note that this sort of thing will work just fine with any kind of simple parameters and return values. The Soap Toolkit will automatically convert strings, integers, floats, Booleans and dates etc. into the proper XML types and pass that data across the wire without a problem. For example, the following simple Web method:

 

[WebMethod]

public DateTime GetServerTime()

{

   return DateTime.Now;

}

 

when called with:

 

lcWSDL = "http://localhost/FoxWebServiceInterop/" + ; 

         "FoxInteropService.asmx?WSDL")

 

o = CREATEOBJECT("MSSOAP.SoapClient30")

o.MSSoapInit(lcWSDL)

ltTime = o.GetServerTime()
? VARTYPE(ltTime)   && T

 

will return a DateTime value natively. Conversely any DateTime parameters you pass in are automatically marshaled to the server in the proper type. This is really one of the big benefits of using Web Services over simply firing XML at a server (which you can do BTW using HTTP GET or POST as opposed to SOAP as the protocol). It provides a simple interface both on the client and the server to have the two sides communicate with simple method interfaces.

 

However, things get more tricky when you’re not dealing with simple values. When passing more complex types like objects or datasets the client and the server have to know what these types are in order to marshal the content around. Both have to agree on the schema and the protocol to serialize the object over the wire.

Getting friendly with Data

Let’s start with a special and very common case for transferring data between .Net and Visual FoxPro. The .Net DataSet has become a very common structure both for .Net and Visual FoxPro. In .Net the DataSet is ADO.Net’s primary data container that holds data retrieved from queries. It is ADO.Net’s primary data interface that is accessed by the user interface. In Visual FoxPro DataSets are not directly supported, but Visual FoxPro 8.0 introduced the XMLADAPTER class which allows you to easily convert to and from DataSets and VFP Cursors.

 

To demonstrate how we can utilize a DataSet to provide offline data editing through a Web Service I’ll use a simple form based example using the trusted old Sql Server Pubs sample database. I’ll start by retrieving a DataSet from .Net into a Visual FoxPro cursor over the Web Service.

 

Listing 1 – Returning a DataSet from the Web Service using a Business Object

[WebMethod]

public DataSet GetAuthors(string Name)

{

      busAuthor Author = new busAuthor();

      int Result = Author.GetAuthors(Name);

 

      if (Author.Error)

            throw new SoapException(Author.ErrorMsg,;

                       SoapException.ServerFaultCode);

 

      if (Result < 0)

      {

            return null;

      }

 

      return Author.DataSet;

}

 

Listing 1 demonstrates what the Web Service method looks like on the .Net Server Side with C# code. Notice that I opted to use a business object here to retrieve the DataSet. The business object runs the actual SQL to retrieve the Author data and stores the resulting DataTable in the DataSet property.

 

Listing 2 – The Business Object method is using the business object base class

public int GetAuthors(string Name)

{

      if (Name == null)

            Name = "";

 

      Name += "%";

      return this.Execute("select * from " + this.TableName +
                          " where au_lname like @Name",
                          "AuthorList",
                          new SqlParameter("@Name",Name) );

}

 

Listing 2 demonstrates the business object method which relies on the SimpleBusObj base class to handle the actual Execution of the Sql statement. busAuthor inherits from SimpleBusObj which includes the Execute() method to run a generic SQL command and return a result into the business object’s DataSet property. Specifically in this case it generates an AuthorList Table in this DataSet. You can take a look at the included source code to check out the details of this reusable Sql code.

The DataSet stored in the Authors.DataSet property is then sent wholesale back to the client via the Web Service method.

 

The Web Service method basically wraps the business object method, but it provides a number of modifications in the way it presents itself to the world from the underlying business method. As you can see it’s returning a DataSet instead of the count of records since a Web Service method needs to be stateless, and it throws a SoapException when an error occurs so that the client can know what went wrong with the Service call. I’ll talk more about Error handling later on in the article.

 

The main change is the DataSet result which provides the marshalling mechanism to send the data to the client. Sending a DataSet is the easiest way to share this data with Visual FoxPro. You could also pass the entire business object, but VFP wouldn’t know what to do with this object and you’d have to manually parse it on the VFP end. But you can easily pass a DataSet which can contain one or more tables of data embedded inside of it which in most cases is sufficient for the client side application.

 

Ok, let’s see what it takes on the Visual FoxPro end to pick up this DataSet. This is not as simple as it was with the result value from our previous methods because in essence we are returning an object from .Net. Worse, the SOAP Toolkit has no idea how to deal with this object.

 

o = CREATEOBJECT("MSSOAP.SoapClient30")

o.MSSoapInit(lcWSDL)

loResult = o.GetAuthors("")

 

You will find that loResult returns an object. Specifically loResult will contain an XMLNodeList object that points at the SOAP result node. This seems like an odd choice, but this is how MSSOAP returns all embedded object types. Even so, you can trace backwards to retrieve the XML of the embedded Response (in this case the DataSet) XML or even the entire SOAP XML document.

 

You can retrieve the the entire SOAP Response XML as an XML string with this code:

 

lcXML = loResult.item(0).ownerDocument.Xml

 

Note that the returned object is of type NodeList which is a collection of Node objects. We have to drill into the first one (if it exists) and from there can walk backwards up the hierarchy to retrieve the ownerDocument, or the parentNode.

 

The parentNode is really what we’re interested in as the parentNode contains the actual XML for the our return value which is in this case our DataSet.

 

lcXML = loResult.item(0).parentNode.Xml

 

The XML for this result is shown in Listing 3.

 

Listing 3 – Full XML SOAP response for a DataSet

<GetAuthorsResult xmlns="http://www.west-wind.com/FoxWebServiceInterop/">

   <xs:schema id="NewDataSet" xmlns=""

    xmlns:xs="http://www.w3.org/2001/XMLSchema"

    xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">

      <xs:element name="NewDataSet" msdata:IsDataSet="true">

         <xs:complexType>

            <xs:choice maxOccurs="unbounded">

               <xs:element name="AuthorList">

                  <xs:complexType>

                     <xs:sequence>

                        <xs:element name="au_id"

                                    type="xs:string" minOccurs="0" />

                        <xs:element name="au_lname"

                                    type="xs:string" minOccurs="0" />

                        <xs:element name="au_fname"
                                   
type="xs:string" minOccurs="0" />

                        <xs:element name="phone"

                                    type="xs:string" minOccurs="0" />

                        <xs:element name="address"

                                    type="xs:string" minOccurs="0" />

                        <xs:element name="city"

                                    type="xs:string" minOccurs="0" />

                        <xs:element name="state"

                                    type="xs:string" minOccurs="0" />

                        <xs:element name="zip"

                                    type="xs:string" minOccurs="0" />

                        <xs:element name="contract"

                                    type="xs:boolean" minOccurs="0" />

                     </xs:sequence>

                  </xs:complexType>

               </xs:element>

            </xs:choice>

         </xs:complexType>

      </xs:element>

   </xs:schema>

   <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"

              xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">

      <NewDataSet xmlns="">

         <AuthorList diffgr:id="AuthorList1"

                     msdata:rowOrder="0">

            <au_id>648-92-1872</au_id>

            <au_lname>Blotchet-Halls</au_lname>

            <au_fname>Reginald            </au_fname>

            <phone>503 745-6402</phone>

            <address>55 Hillsdale Bl.</address>

            <city>Corvallis</city>

            <state>OR</state>

            <zip>97330</zip>

            <contract>true</contract>

         </AuthorList>

        

         ... More entries

        

      </NewDataSet>

   </diffgr:diffgram>

</GetAuthorsResult>

 

Note that the root node here is the name of the method plus Result: GetAuthorsResult. This is the root node for the NodeList that is returned to you. In the above document you have two nodes in this nodelist: The xs:schema and diffgram:diffgram nodes which map to loResult.Item(0) and loResult.Item(1) respectively.

 

Sounds complicated but once you’ve done this once or twice it comes easy. This mechanism does give you control since you have access to the entire SOAP packet through it.

 

So, now let’s see how we can retrieve this DataSet returned from .Net into a cursor we can browse. Listing 4 shows the code to do this.

 

Listing 4 – Retrieving the Authors DataSet in Visual FoxPro

LOCAL o as MSSOAP.SoapClient30

o = CREATEOBJECT("MSSOAP.SoapClient30")

 

loException = null

llError=.f.

TRY

   o.MSSoapInit(lcUrl)

CATCH TO loException

   llError = .t.

ENDTRY

 

IF llError

   ? "Unable to load WSDL file from " + lcUrl

   return

ENDIF

 

*** Retrieve authors - returns DataSet returned as NodeList

loException  = null

TRY

   loNL =  o.GetAuthors("")

CATCH TO loException

   llError = .t.

ENDTRY

 

*** Check for SOAP Error first  - set even if Exception

IF (o.FaultCode != "")  && Client or Server (usually Server)

   ? o.FaultString

   return

ENDIF

 

*** If no SOAP Error check Exception

IF !ISNULL(loException)

   ? loException.Message

   ? loException.ErrorNo

   RETURN

ENDIF

 

*** Call went throuh OK

 

*** Grab the XML - should be a dataset

loDOM = loNL.item(0).parentNode

lcXML = loDOM.Xml

 

LOCAL oXA AS XMLADAPTER

oXA = CREATEOBJECT("XMLAdapter")

oXA.LOADXML(lcXML,.F.,.T.)  && from string

* oXA.Attach(loDOM)   && Prefer using the DOM

 

IF (USED("Authors"))

  USE IN AUTHORS

ENDIF

 

oXA.TABLES[1].TOCURSOR(.F.,"Authors")

 

BROWSE

 

I’ve included rudimentary error checking in this code. Remember that you are pulling data from a Web Service so it’s a very good idea to wrap every call to it in an exception block to make sure that you capture any potential errors and handle them accordingly. I have more on this later.

 

Once you’ve retrieved the DataSet we still need to convert it using the XMLAdapter object. XMLAdapter was introduced in Visual FoxPro 8 and allows you to load an XML document and among other things convert it back into a cursor. You can load XML from a URL, a DOM object or an XML string. The code above shows how to get at a DOM node or an XML string from the returned data. In this case, since a DOM node is available let’s use it and use the oXA.Attach(loDOM) method to load the DataSet Xml.

 

Note that using a DOM object won’t always work due to the fact that the SOAP Toolkit and XMLAdapter don’t always use the same version of the XMLDOM. In my experience, I got flakey results where sometimes I’d get an error and others not – I can’t really explain why other than that the SOAP Toolkit was not consistent in returning XMLDOM 4.0 documents that the XMLAdpater requires. The workaround for this is to load the XML as a string using LoadXml():

 

oXA.LOADXML(lcXML,.F.,.T.)  && from string

 

even though this incurs a little more overhead.

Not quite there yet

If you run the code above you should end up with a cursor for the SQL Server Authors table. But this result probably won’t satisfy you – the result you get is formatted wrong with all fields showing up as Memo fields. The problem here is that we’re retrieving generic type data from that DataSet. Type Data that is generated on the fly. If you look at Listing 3 again and look at the XML schema you can see that the types are reported but none of the lengths are. So any string value is returned as a Memo field, any numeric value as a generic number and so on.

 

To get complete type information we need to make a change to how ADO.Net retrieves our data from the Database. In our business object code I can add this directive:

 

busAuthor Author = new busAuthor();

              

// *** Force Complete Schema to be loaded
Author.ExecuteWithSchema = true;

 

This translates into ADO.Net code that tells an ADO.Net DataAdapter to retrieve data with full schema information:

 

SqlDataAdapter Adapter = new SqlDataAdapter();

if (this.ExecuteWithSchema)

   Adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;


Adapter.SelectCommand = new SqlCommand(SQLCommandString,Conn);

 

This code lives in the SimpleBusObj class’ Execute() method which executes a SQL command. I made ExecuteWithSchema an optional flag on the business object, so that I can choose when to include the schema. Normally you don’t want to load Schema information because it causes extra data to be sent down from SQL Server. But in Web Service front ends like our sample WebService here, we always want the schema to be there so that disconnected clients can properly parse the DataSet into local data structures that get dynamically created such as Visual FoxPro cursors. As an alternative you can use pre-created tables with the XMLAdapter in which case the tables act as the ‘schema’ to figure out the data types.

 

Updating the Data on the Client

Now that we have the data on the client, what can we do with it? Well, it’s a VFP writable cursor so you can party on the data all you want! You can edit, update and add new records to your heart’s content. But remember that the data is completely disconnected. We downloaded a DataSet from the server and we turned it into a cursor, but there’s no direct connection between this cursor and the Web Service or the Author data source sitting on the server.

 

So in order to be able to work with this data offline and sync with the server we need to track the changes we’re making and then submit those changes back to the Web Server. Using the tools in Visual FoxPro 8 this involves the following steps:

 

  1. Setting multi-row buffering on in the table
  2. Working with the Cursor and updating the data as needed
  3. Using the XMLAdapter to generate a DiffGram DataSet of the changed data
  4. Sending the Diffgram XML back to the server

 

The first step after the data has been downloaded as shown in Listing 4 is to enable multi-row buffering in the new cursor so that changes can be tracked. You do this with these two simple lines of code:

 

SET MULTILOCKS ON

CURSORSETPROP("Buffering",5)

 

At this point you’re free to make changes to the Cursor as needed. When it comes time to update the data the first step is to generate a DiffGram from this data using the XML Adapter. You’ll want to use the XML Adapter so you can return XML in a format that is similar to the DataSet’s GetChanges() method in ADO.Net. Listing 5 shows how to do this with the Authors table in the Sample application.

Listing 5 – Creating an XML Diffgram with the XMLAdapter

LOCAL oXA as XMLAdapter

oXA = CREATEOBJECT("XMLAdapter")

oXA.UTF8Encoded = .t.

oXA.AddTableSchema("Authors")

oXA.IsDiffgram = .T.

 

lcXML = ""

oXA.ToXML("lcXML",,.f.,.T.,.T.)

 

RETURN lcXML

 

The key function here is AddTableSchema and the IsDiffGram property. AddTableSchema adds a table to the XML Adapter for processing. IsDiffGram makes sure that when you execute any ToXml() methods that the XML is generated in DiffGram format – it only shows rows that have changed. Listing 6 shows the content of this DiffGram XML with a single record changed.

 

Listing 6 – DiffGram Code generated by the XML Adapter

<?xml version = "1.0" encoding="Windows-1252" standalone="yes"?>

<VFPData>

      <xsd:schema id="VFPDataSet"             

       xmlns:xsd="http://www.w3.org/2001/XMLSchema"

       xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">

 

… Data Structure Schema ommitted for space

      </xsd:schema>

      <diffgr:diffgram

        xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"

        xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">

            <VFPDataSet xmlns="">

                  <Authors diffgr:id="authors_1" msdata:rowOrder="1"

                           diffgr:hasChanges="modified">

                        <au_id>648-92-1872</au_id>

                        <au_lname>Blotchet-Halls II</au_lname>

                        <au_fname>Reginald</au_fname>

                        <phone>503 745-6402</phone>

                        <address>55 Hillsdale Blvd.</address>

                        <city>Corvallis</city>

                        <state>OR</state>

                        <zip>97330</zip>

                        <contract>true</contract>

                  </Authors>

            </VFPDataSet>

            <diffgr:before>

                  <Authors diffgr:id="authors_1"

                           msdata:rowOrder="1" xmlns="">

                        <au_id>648-92-1872</au_id>

                        <au_lname>Blotchet-Halls</au_lname>

                        <au_fname>Reginald</au_fname>

                        <phone>503 745-6402</phone>

                        <address>55 Hillsdale Bl.</address>

                        <city>Corvallis</city>

                        <state>OR</state>

                        <zip>97330</zip>

                        <contract>true</contract>

                  </Authors>

            </diffgr:before>

      </diffgr:diffgram>

</VFPData>

 

As you can see the XMLAdapter is pretty verbose in its creation of the DiffGram – it includes a full schema of the data structure and includes a full record for each change instead of just the fields. If you’ve used the XMLUPDATEGRAM() function in VFP before you might be surprised at the amount of XML generated. Unfortunately all of this is required in order for .Net to be able to create a DataSet from your data in the same format as you would get from the DataSet.GetChanges() method which also produces a ‘diffgram’ DataSet.

Passing XML DataSets back to the server

Now that we have our XML DiffGram we need to pass it back to the server. We have a few problems here with Visual FoxPro that makes this process a little less than optimal. The problem is that the MSSOAP Toolkit does not deal well with objects passed from the Client to the server in a SOAP method. There’s no problem passing simple types, but the SOAP client cannot convert a Fox (or other COM object) into something that can be turned into a SOAP representation automatically. There are ways that you can do this by creating a COM object and generating a custom WSDL file, but this is a lot of work and in most cases not worth the effort.

 

So with this limitation in mind you have a couple of options for passing Complex Objects and DataSets: Passing an XMLNodeList which like the return parameters is properly parsed into the SOAP document as raw XML. Or you can pass the data as a string to the server, but this requires that the Web Service includes a separate method that knows how to accept a string instead of an object.

 

Let me demonstrate the former in all the gory details. Let’s start by creating a Web Service method that can accept our changes in the form of a DataSet as shown in Listing 7. The DataSet in this case will be a DataSet of our DiffGram XML we generated.

 

Lising 7 -  A Web Service method that saves changes made on the client

[WebMethod]

public bool UpdateAuthorData(DataSet Ds)

{

      busAuthor Author = new busAuthor();

     

      Author.DataSet = Ds;

 

      bool Result = Author.Save("Authors");

 

      return Result;

}

 

 

// *** Business object Save Behavior (separate source file SimpleBusObj.cs)

public bool Save(string Table)

{

      if (!this.Open())

            return true;

 

      SqlDataAdapter oAdapter =

     new SqlDataAdapter("select * from " + this.TableName,this.Conn);

 

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

      SqlCommandBuilder oCmdBuilder = new SqlCommandBuilder(oAdapter);

 

      int lnRows = 0;

      try

      {

            /// sync changes to the database

            lnRows = oAdapter.Update(this.DataSet,Table);

      }

      catch(Exception ex)

      {

            this.SetError(ex.Message);

            return false;

      }

      finally

      {

            this.Close();

      }

 

      if ( lnRows < 1 )

            return false;

 

      return true;

}

 

The Web Service method is pretty simple as it once again uses the base functionality in the business object base class (shown on the bottom for completeness) to persist its changes. The base behavior is pretty straight forward using the stock DataAdapter/CommandBuilder syntax that automatically builds the appropriate update/insert/delete statements for any updated or added records in the DataTable.

 

If we want to call this method from Visual FoxPro over the Web Service we need to first get our DiffGram XMLADAPTER Xml returned to us, and then figure out how to to pass the DataSet as an object – not as a simple string parameter – to the Web Service.

 

The MSSOAP Client allows sending of raw XML in the form of an XMLNodeList, which is a collection of childnodes in the DOM. You basically need to provide the nodes underneath the Document Element node of the document, which can be retrieved by using:

 

loNL = loDom.DocumentElement.ChildNodes()

 

This NodeList can now be passed as the parameter for a complex type or in this case a .Net DataSet. If we put all the pieces together the code looks like shown in Listing 8.

 

Listing 8 – Passing a DataSet object to a .Net Web Service via XML

oSOAP = CREATEOBJECT("MSSOAP.SoapClient30")

oSOAP.MSSoapInit(lcWSDL)

 

SELE AUTHORS  && Assume Authors is loaded

 

*** Generate the DataSet XML

LOCAL oXA as XMLAdapter

oXA = CREATEOBJECT("XMLAdapter")

oXA.UTF8Encoded = .t.

oXA.AddTableSchema("Authors")

oXA.IsDiffgram = .T.

lcXML = ""

oXA.ToXML("lcXML",,.f.,.T.,.T.)

 

*** Convert the XML into a NodeList

loDOM = CREATEOBJECT("MSXML2.DomDocument")

loDom.LoadXMl(lcXML)

loNodeList = loDom.DocumentElement.ChildNodes()

 

*** Retrieve authors - returns DataSet returned as NodeList

LOCAL loException as Exception

loException  = null

 

TRY

   *** Pass the NodeList as the DataSet parameter

   llResult =  oSOAP.UpdateAuthorData(loNodeList)

CATCH TO loException

   llError = .t.

ENDTRY

 

*** Always check for errors

IF !ISNULL(loException)

   lcError = oSOAP.FaultString

   if EMPTY(lcError)

      lcError = loException.Message

   ENDIF

   ? lcError

   ? loException.ErrorNo

   RETURN

ENDIF

 

*** Print the reslt - .t. or .f.

? llResult

RETURN

 

There are a couple of important pieces in that code. First when you use the XMLAdapter always make sure that you use UTF8Encoded = .T.. The XMLAdapter by default generated Ansi-1252 XML which will fail inside of the SOAP Envelope it gets embedded into if you have any extended characters in your data. SOAP usually encodes in UTF-8 so make sure your generated XML matches that.

 

You have to call ToXML() and then load the resulting XML into an XMLDOM object. From there you navigate down to the ChildNodes() of the DocumentElement which provides the NodeList parameter that the SOAPClient uses to embed the XML into the document as an object – a DataSet object that the .Net Web Service will recognize as a DataSet.

Rethinking Data Access

Ok now that we have the basics down let’s think about what we’re doing here. We’re downloading data to the client side and we’re running this data in a complete offline scenario potentially making changes to many records at a time.

 

You might think right about now, what happens in conflict scenarios? Well, the answer it’s not pretty. Basically the stock .Net update mechanism will run updates until a failure occurs then fail and return an error. This means by default at least you’re updating some records, but not all of them, which is probably not a good way to go. ADO.Net supports the ability to see exactly what failed by using the HasErrors property on each DataRow in a DataTable as well as on the DataTable. There is also a ContinueUpdateOnError method on the DataAdapter that allows you to submit all records and mark only the failed ones instead of the default behavior that just stops (who thought that this would be useful?). I’m not going to go into detail on the complex topic of conflict resolution – I would point you to Chapter 11 of David Sceppa’s excellent ADO.Net book from Microsoft Press if you want to see a number of different ways to deal with update conflicts.

 

What I’m getting at is that ADO.Net has strong features for detecting and negotiating update errors, but it requires fairly complicated logic to make this work well. Doing this sort of thing remotely over a Web Service becomes even more complex because you can’t easily pass the error information and detail data back and forth and in most cases there’s no state that would allow you to actually deal with conflicts based on a standard ADO.Net DataSet scheme.

 

So what to do? First is to review whether update conflicts are really an issue in your application. In many applications update conflicts are extremely rare and when they do occur a last one wins is often not unreasonable.

 

The next thing is to try and make sure that you issue data updates as atomically as possible. In other words try to make sure you read data, edit it and write it back immediately instead of hanging on to it on the client. If your application is designed with always-on in mind then this approach is reasonable.

.

 

Let’s examine what this looks like with the sample application and code I showed so far. So far, I pulled down all the customer data and then allowed the client to make changes to the data – multiple records and send the changes back up to the server. First pulling down ALL of the data in a huge batch is probably not a good idea as it takes time to pull this data down, so a better approach might be to pull down just a list of all the authors and then cause each click on an author to download the current author information one record at a time.

 

To demonstrate the scenarios I created two samples – one using batch updates and one using atomic updates in two separate form based applications that use the FoxInteropService Web Service. Figure 4 shows the Atomic version.

 

Figure 4 – The Web Service Authors sample form demonstrates atomic data access

 

To handle the Atomic scenario, I added a couple of methods to the Web Service shown in Listing 9.

 

Listing 9 – Building a more atomic Data Access Service. Retrieving a list and individual authors

[WebMethod]

public DataSet GetAuthor(string ID)

{

      busAuthor Author = new busAuthor();

      Author.ExecuteWithSchema = true;

 

      if ( !Author.Load(ID) )

            throw new SoapException(Author.ErrorMsg,SoapException.ServerFaultCode);

 

      // *** Must return entire Data Set

      // *** will be in the 'Authors' table

      return Author.DataSet;

}


[WebMethod]

public DataSet GetAuthorsList()

{

      busAuthor Author = new busAuthor();

      Author.ExecuteWithSchema = true;

 

      int Result = Author.GetAuthors("","au_id,au_lname,au_fname",

                                     "AuthorList");

 

      if (Author.Error)

            throw new SoapException(Author.ErrorMsg,

                                    SoapException.ServerFaultCode);

 

      if (Result < 0)

            return null;

 

      return Author.DataSet;

}

 

GetAuthor retrieves a single Author which will be used to pull the author data as the user navigates to it in the list. GetAuthorsList() pulls down just the data that is displayed in the list – ie. the first and last names as opposed to all of the data for each record. For the Authors table this would not be a big difference to pulling all of the data, but consider a list of complex invoices with many fields where the display fields would be a tiny percentage of the overall size of the data pulled.

 

Note that I didn’t create a new UpdateAuthors method – the same logic used previously will still work now, because although we now will be updating a single record instead of multiples, we’re still working with a DataSet that updates the server. The update logic is identical.

 

On the Fox End the Authors form now includes the following Init code shown in Listing 10:

 

Listing 10 – Initializing the SOAP client on the Author Form

* FUNCTION Form.Init 

LPARAMETERS lcWSDLURL

 

IF EMPTY(lcWSDLURL)

   lcWSDLURL = "http://localhost/foxwebserviceInterop/foxinteropservice.asmx?WSDL"

ENDIF

 

SET PROCEDURE TO wwUtils ADDITIVE

SET DELETED On

SET SAFETY OFF

SET TALK OFF

SET EXACT OFF

 

THISFORM.oNet = CREATEOBJECT("MSSOAP.SoapClient30")

THISFORM.oNet.MSSOAPInit(lcWSDLURL)

 

*** Let SOAP inherit our System Proxy Settings

thisform.oNet.ConnectorProperty("ProxyServer")="<CURRENT_USER>"

thisform.oNet.ConnectorProperty("ConnectTimeout")=5000

thisform.oNet.ConnectorProperty("Timeout")=10000

 

IF THISFORM.LoadList()

      THISFORM.oCustList.Value = 1

  

   lcID = AuthorList.au_id

   thisform.LoadAuthor (lcID)

ENDIF

 

THISFORM.BindControls = .T.

RETURN

 

Note that I pass in the WSDL file as a parameter so that I can easily switch between test and production environments if necessary. I use a Form property to hold a reference to the SOAP Client, so we can reuse this single instance to make multiple Web Service calls. Keep in mind that the call to MSSOAPInit always retrieves the WSDL file and forces parsing of the WSDL which has some overhead. If your app calls the same Web Services frequently you’ll want to cache that reference somewhere for reuse as I am doing here with a form property.

 

Notice the use of the ConnectorProperty which allows you to configure the HTTP Connection for the client. This method allows you set things like the Proxy Server, Authentication information Timeouts and more as well as providing the ability to modify the HTTP Header sent to the server. Using the ProxyServer property with the “<CURRENT_USER>” value is a good idea to make the client use the configured Proxy settings from Internet Explorer. Using this default should handle most proxy scenarios and allow you to get away without any custom configuration via code. Even so, custom configuration can be done via other properties available on the HttpConnector object. For more information on what’s available see the HTTPConnector30 topic in the MSSOAP Help file.

 

Once initialized we’re ready to make calls against the Web Service and this is done in two methods, LoadList() which loads the listbox and LoadAuthor which loads an individual Author.

 

Listing 11 – Loading an AuthorList and individual author from the Web Service

**********************************************************

FUNCTION LoadList

*****************

 

*** Retrive the data from the COM object as an XML string

llError = .F.

this.cErrorMsg = ""

 

TRY

   loNL = THISFORM.ONET.GetAuthorsList()

CATCH

      llError = .T.

      this.cErrorMsg = this.oNet.FaultString        

      IF EMPTY(THIS.cErrorMsg)

         THIS.cErrorMsg = loException.Message

      ENDIF     

ENDTRY

 

*** Always check for errors

IF llError

            MESSAGEBOX("Error Loading Data" +CHR(13) + ;

                       this.cErrorMsg)

      RETURN .F.

      ENDIF

 

IF USED("AuthorList")

   USE IN AuthorList

ENDIF

 

lcXML = loNL.Item(0).ParentNode.Xml

 

*** Convert the data downloaded into a cursor from the XML

LOCAL oXA AS XMLADAPTER

oXA = CREATEOBJECT("XMLAdapter")

oXA.LOADXML(lcXML,.F.,.T.)

 

oXA.TABLES[1].TOCURSOR(.F.,"AuthorList")

 

*** Set the Buffermode to 5 for the cursor

*** so we can send diffgrams

SET MULTILOCKS ON

CURSORSETPROP("Buffering",5)

 

RETURN .T.

 

**********************************************************

FUNCTION LoadAuthor

*******************

LPARAMETERS lcID

 

llError = .F.

this.cErrorMsg = ""

 

loException = null

TRY

   loNL = this.oNet.GetAuthor(lcID)

CATCH

      llError = .T.

      this.cErrorMsg = this.oNet.FaultString        

      IF EMPTY(THIS.cErrorMsg)

         THIS.cErrorMsg = loException.Message

      ENDIF     

ENDTRY

 

*** Always check for errors

IF llError

      MESSAGEBOX("Error Loading Author" +CHR(13) + ;

                 this.cErrorMsg)

      RETURN .F.

ENDIF

 

IF USED("Authors")

   USE IN Authors

ENDIF

 

lcXML = loNL.Item(0).ParentNode.Xml

 

*** Convert the data downloaded into a cursor from the XML

LOCAL oXA AS XMLADAPTER

oXA = CREATEOBJECT("XMLAdapter")

oXA.LOADXML(lcXML,.F.,.T.)

 

oXA.TABLES[1].TOCURSOR(.F.,"Authors")

 

*** Set the Buffermode to 5 for the cursor

*** so we can send diffgram for this single record

SET MULTILOCKS ON

CURSORSETPROP("Buffering",5)

 

THISFORM.Refresh()

 

This code is similar to what we saw before. The code for both of these methods is similar too as they both pull data to the client in the same way: By pulling down a dataset.

 

Notice also that I always use error handling around the Web Service, which is crucial. Since you have no control over the Web Service you always want to check the result from the Web Service as well as trapping any requests in an Exception block. You can retrieve server side error messages by reading the FaultString property on the SoapClient object. Note that the server must throw a SoapException in .Net in order for a clean message to be retrieved on the client side:

 

bool Result = Author.Save("Authors");

if (!Result)

      throw new SoapException(Author.ErrorMsg,SoapException.ServerFaultCode);

 

If a simple Exception is thrown or an unhandled error occurs on the server you will end up with a stack trace on the client instead, which is pretty much useless other than for debugging. Therefore it’s very important that your Web Service handles all exceptions itself and re-throws any internal exceptions as SoapExceptions so that the client can pick it up.

 

Finally the SaveAuthors method is called when the user updates a single record as shown in Listing 12.

 

Listing 12 – Saving an Author to the Web Service

*** Don't update if we don't have to

IF GETNEXTMODIFIED(0) = 0

   WAIT WINDOW "Nothing to save..." NOWAIT

   RETURN

ENDIF  

 

llError = .f.

thisform.cErrorMsg = ""

Result = .F.

 

*** Retrieve the Diffgram XML

lcXML = thisform.GetDiffGram()

 

loDOM = CREATEOBJECT("MSXML2.DomDocument")

loDOM.LoadXML(lcXML)

loNL = loDOM.DocumentElement.ChildNodes()

 

TRY

    Result = thisform.oNet.UpdateAuthorData(loNL)

CATCH

   llError = .T.

   thisform.cErrorMsg = thisform.oNet.FaultString

ENDTRY

 

IF llError

   MESSAGEBOX(thisform.cErrorMsg,0 + 48,"Update Error")

   RETURN

ENDIF

 

IF !Result

   MESSAGEBOX("Unable to save Customers",0+48,"Update Error")

   return

ENDIF

 

*** Update our table state so further updates

TABLEUPDATE(.T.)

 

*** Force the form to rebind

THISFORM.Refresh()

 

WAIT WINDOW "Author Saved..." TIMEOUT 4

RETURN

 

*************************************************************

FUNCTION GetDiffGram

********************

LPARAMETERS llReturnXmlAdapter

 

LOCAL oXA as XMLAdapter

oXA = CREATEOBJECT("XMLAdapter")

OXA.UTF8Encoded = .t.

 

oXA.AddTableSchema("Authors")

oXA.IsDiffgram = .T.

 

IF llReturnXmlAdapter

  RETURN oXA

ENDIF

 

lcXML = ""

oXA.ToXML("lcXML",,.f.,.T.,.T.)

 

RETURN lcXML

 

Note that the first step here is to make sure that we actually have an update to do. If you send an empty dataset to the server you will actually get an error, aside from the fact that you’re wasting bandwidth when you do this. GetDiffGram then uses the XMLAdapter to generate the XML for the changed or inserted record. We then generate the NodeList and send it up to the server with the same UpdateAuthors() method we called previously on the Web Service.

 

Note that once the update is done we have to call TableUpdate() to reset our internal tracking of the table buffer. Remember we’re not writing the data locally, but on the server, but we have to keep the update state in sync on the client side as well, so TableUpdate() is required here so if we make another change to another record we don’t end up with both of them sent up to the server.

 

The final piece is adding a new record. Adding a new record is really just as simple as inserting a new record into the Authors table as shown in Listing 13. All we have to make sure of is that the buffering is properly set in this scenario.

 

Listing 13 – Adding a new record inserts a local record. It’s written to the server when we save.

lcSoc = INPUTBOX("Please Enter the Social Security number:")

IF EMPTY(lcSoc)

   RETURN

ENDIF

 

SELECT AUTHORS

 

*** Throw away other changes so we can start over

TABLEREVERT(.T.)

 

APPEND BLANK

REPLACE AU_ID WITH lcSoc

 

THISFORM.Refresh()

 

Atomicity is very common in Web Service as you want to minimize the amount of data that comes over the wire as well minimizing the possibility of update conflicts. As it turns out if you browse the data heavily it’s quite possible that the bandwidth requirements are actually more than using the all at once approach, but the big advantage is that you’re not tying up the server for long requests by retrieving data in small chunks. This keeps applications responsive and allows your client application to provide constant feedback. In this respect, Web Service based applications are very different from Windows applications and require a different thinking about data unlike desktop apps where we often rely on instant access and the ability to statefully lock data. That luxury is not available for Web based data access…

Other Complex DataTypes

DataSets are very useful in this scenario because they translate really well into FoxPro cursors and back. But DataSets are not an official SOAP data type – the type and format is specific to .Net and other Microsoft tools. If you return a DataSet to a Java Application for example it won’t know what to do with it, as it has no concept of a DataSet. For a Java Application this means the XML has to be manually parsed.

 

You can also return objects from a .Net Web Service and unlike DataSets if your objects are made of simple types you can expect most SOAP clients to be able to consume and make sense of the objects on the client side.

 

Unfortunately for Visual FoxPro (and other COM clients like VB6) complex types mean problems. Specifically Visual FoxPro will not be able to automatically receive a complex object because there’s no automatic mapping mechanism available in the SOAP toolkit. Instead the SOAP Toolkit provides complex objects in the same NodeList format that the DataSet was provided. There’s no automatic parser available for this sort of object in the Visual FoxPro language.

 

Let’s take a look at another example and use the wwSOAP class that can provide some relief with parsing result objects. Rather than returning a customer record as a DataSet row, let’s return the record as an object. Listing 14 shows an AuthorEntity class. To make things a little more interesting I also added a child object that contains Phonenumbers to demonstrate hierarchical objects in this context.

Listing 14 – Creating an Author Entity object that maps the Author fields into an object

[Serializable()]

public class AuthorEntity : SimpleEntity

{                

      public AuthorEntity()

      {}

 

      protected DataRow Row;

 

      public PhoneInfo PhoneNumbers = new PhoneInfo();

 

      public String Au_id;

      public String Au_lname;

      public String Au_fname;

      public String Phone;

      public String Address;

      public String City;

      public String State;

      public String Zip;

      public Boolean Contract;

}

 

[Serializable()]

public class PhoneInfo

{

      public string HomePhone = "808 579-8342";

      public string WorkPhone = "808 579-8342";

      public string Fax = "808 801-1231";

      public DateTime ServiceStarted = Convert.ToDateTime("01/01/1900");

}

 

[Serializable()]

public class SimpleEntity

{

      public void LoadRow(DataRow Row)

      {

            wwDataUtils.CopyObjectFromDataRow(Row,this);

      }

      public void SaveRow(DataRow Row)

      {

            wwDataUtils.CopyObjectToDataRow(Row,this);

      }

 

public ArrayList CreateEntityList(DataTable Table)

      {    

            ArrayList EntityList = new ArrayList();

            foreach(DataRow Row in Table.Rows)

            {

                  // *** Must dynamically create an instance of this type

                  SimpleEntity Entity =

                         (SimpleEntity) Activator.CreateInstance(this.GetType());

                  Entity.LoadRow(Row);

 

                  EntityList.Add( Entity );

            }

 

            return EntityList;

      }

}

 

This is real simple implementation of an Entity class that’s based on an underlying DataRow which is implemented in the LoadRow and SaveRow methods that read and write the data from the object generically back to the underlying DataRow using Reflection.  With this class in place we can now return an object from the Web Service as shown in Listing 15.

 

Listing 15 – Returning Author Data as an object is more light weight

[WebMethod]

public AuthorEntity GetAuthorEntity(string Id)

{

      busAuthor Author = new busAuthor();

 

      if (!Author.Load(Id))

            return null;

 

      AuthorEntity Auth = new AuthorEntity();

      Auth.LoadRow(Author.DataRow);

     

      return Auth;

}


Note that the object passed is not the business object itself, but rather an entity object that serves as a Data Container. The reason for this is that the business object is pretty heavy weight in what it contains. So we really just want to pass down the data graph of this object which in this case is the entity object (which could also be part of the business object itself).

 

This is also consistent with .Net’s client side Web Service model. Any objects that are accessed on the client are treated as Proxy objects, which are mere generated copies of the full objects returned from the Web Service. In other words, a .Net Web Service Client receives an object of type AuthorEntity, but it contains only the properties and fields of the object – none of the methods, events  or other logic.

 

The advantage of using an object return over a DataSet is that this is much more lightweight than sending down a complete DataSet to the client. Objects are persisted into the SOAP package without schemas as the schema is provided in the WSDL document. In theory at least the client application should be able to parse the object based on this WSDL description.

 

To pick up this object in the SOAP Toolkit we can now use trusted code using the SOAP Toolkit:

 

o = CREATEOBJECT("MSSOAP.SoapClient30")

o.MSSoapInit(lcWSDL)

loNL = o.GetAuthorEntity("486-29-1786")

loRoot = loNL.Item(0).ParentNode
lcXML = loRoot.XML

 

With the SOAP Toolkit alone this as far as it will take you. It provides no easy mechanism to take this object XML and provide you a real object reference from it unless you create a custom type mapper which is more of a hassle than parsing the XML in the first place. So, from here you have to manually parse the XML using the XMLDOM to do anything useful with it.

 

 However, there are a couple of alternatives you can use by using the free wwSOAP and wwXML classes from West Wind Technologies. With the XML returned you can use wwXML to parse the XML to an object as long as you have an object that has a matching structure:

 

Listing 16 – Calling a Web Service and retrieving an object with wwXML

oSOAP = CREATEOBJECT("MSSOAP.SoapClient30")

oSOAP.MSSoapInit(lcWSDL)

 

loNL = o.GetAuthorEntity("486-29-1786")

loRoot = loNL.Item(0).ParentNode

 

*** Create an object with the server’s structure

loAuthor = CREATEOBJECT("Relation")

loAuthor.AddProperty("Au_id","")

loAuthor.AddProperty("Contract",.f.)

loAuthor.AddProperty("PhoneNumbers",CREATEOBJECT("RELATION"))

loAuthor.PhoneNumbers.AddProperty("HomePhone","")

loAuthor.PhoneNumbers.AddProperty("ServiceStarted",DATETIME())

 

oXML = CREATEOBJECT("wwXML")

oXML.lrecurseobjects = .t.

 

*** Parse XML into loAuthor object with CaseInsensitive Parsing

oXML.ParseXmlToObject(loRoot,loAuthor,.t.)

 

? loauthor.au_lname

? loAuthor.PhoneNumbers.HomePhone

? loAuthor.PhoneNUmbers.ServiceStarted

 

wwXML includes a number of parsing methods that can create XML from an object and vice versa. Here one of the low level methods ParseXmlToObject() is used to turn XML into an object that has been provided. wwXML’s object parsing can work without a Schema as long an object is provided as input – the input object serves as the Schema in this case. wwXML parses through the object properties and looks for the corresponding XML elements to find a match. It retrieves the string value and converts it to the proper type.

 

Note that the last .T. flag in the call specifies that the parsing is to occur case insensitively. VFP property names are never case sensitive (they’re always lower case), but of course XML nodes are. This poses a big problem as it’s difficult to match up a VFP object’s properties with those of a .Net object that uses common CamelCase for property names. I REALLY wish VFP would support proper casing for objects internally! Anyway, the workaround is to use the llParseCaseInsensitive flag which is more time consuming as wwXML loops through all elements for each property to find a match – XPATH has no way to do case-insensitive searches. Bad as it sounds this parsing is relatively fast because objects property lists tend to be fairly small. On single objects this certainly won’t be a problem, but it might get slow if you’re parsing many objects in a loop.

Making Life easier for Object Parsing with wwSOAP

If you want to get an object created for you on the fly rather than pre-creating one you can use the wwSOAP class in many cases. wwSOAP includes a method called ParseObject() which can take a DOM element as input and parse an object based on the WSDL description.

 

o = CREATEOBJECT("MSSOAP.SoapClient30")

o.MSSoapInit(lcWSDL)

loNL = o.GetAuthorEntity("486-29-1786")

 

loSoap = CREATEOBJECT("wwSoap")

loSOAP.Parseservicewsdl(lcWSDL,.t.)

loAuthor = loSOAP.ParseObject(loNL.Item(0).ParentNode,

                              "AuthorEntity")

? loauthor.au_lname

? loAuthor.PhoneNUmbers.ServiceStarted

 

You first call ParseServiceWsdl to parse the WSDL file which creates an internal structure that describes the methods and object types available. The ParseObject method then is called with a type name as the second parameter. If the type is found wwSOAP creates the type based on the WSDL structure and updates the values from the XML structure (using wwXML as shown above internally).

 

If you want to take this one step further you can skip using the MSSOAP client altogether and use the wwSOAP class and a few helper classes to return data to you directly. Let’s call this same Web Service one more time with wwSOAP as shown in Listing 17.

 

Listing 17 – Using wwSOAP to handle object return values automatically

oSOAP = CREATEOBJECT("wwSOAP")

oSOAP.nHttpConnectTimeout = 5

oSOAP.lParseReturnedObject = .T.

 

oSOAP.AddParameter("Id","486-29-1786")

loAuthor = oSOAP.CallWSDLMethod("GetAuthorEntity",lcWSDL)

 

IF oSOAP.lError

   ? oSOAP.cErrorMsg

ENDIF

 

? loauthor.au_lname

? loAuthor.PhoneNumbers.HomePhone

? loAuthor.PhoneNUmbers.ServiceStarted

 

This is lot easier – this mechanism automatically creates the underlying object. The lParseReturnedObject property determines whether any objects are parsed on return, which makes wwSOAP try to parse the returned object. It does this by looking at the WSDL description, creating an object on the fly and reading values in one at a time. This works with plain or nested objects, but it doesn’t work with collections or arrays at this time. If there are any arrays or collections in the object returned these won’t be parsed properly.

 

If lParseReturnedObject is set to .F. or wwSOAP can’t find a matching object structure in the WSDL file it returns an XMLDOMNode. Unlike MSSOAP wwSOAP returns the top level node object of the Return value (to get the same value as MSSOAP returns you can use loNode.ChildNodes()), since this makes much more sense for further parsing if you’re using tools like wwXML or you’re manually walking through the XML using the XMLDOM.

 

wwSOAP also handles exceptions internally – you can check the lError and cErrorMsg properties instead to retrieve error information. In addition wwSOAP also makes it easy to retrieve the Request and Response Xml via a simple property for easier debugging.

Updating the Business object with an Object parameter

Ok, this solves one part of the problem – retrieving parameters. But what about sending them to the server? wwSOAP can help with this task as well, but it takes a little more work.

 

Mapping Visual FoxPro objects to WSDL structures is a mess because VFP doesn’t support mixed case property values. So there’s not a one to one mapping from VFP type to the XML object structure. wwSOAP however provides a method called ParseObject() which uses the WSDL to construct an object dynamically on the fly adding properties to the object and then reading the values from the XML into it.

 

 

LOCAL loSoap as wwSOAP

loSoap = CREATEOBJECT('wwSoap')

loSoap.Parseservicewsdl(lcWSDL,.T.)

 

loAuthor = GetAuthorProxyObject()

loAuthor.au_lname = "Strahl"

loAuthor.au_fname = "Rick"

loAuthor.Phone = "808 123-1211"

loAuthor.Address = "32 Kaiea Place"

loAuthor.City = "Paia"

loAuthor.State = "HI"

loAUthor.Zip = "96779"

loAuthor.Contract = 0

 

loAuthor.PhoneNumbers.HomePhone = "(808) 579-3121"

loAuthor.PhoneNumbers.ServiceStarted = DateTime()

 

lcXML = loSOAP.CreateObjectXmlFromSchema(loAuthor,"AuthorEntity")

 

CreateObjectXmlFromSchema looks at the WSDL file and then parses the specified object from the passed in object reference. The result of this call is shown in Figure 20 - notice the proper casing of the XML. The only downside here is that wwSOAP requires access to the WSDL file so if you’re using MSSOAP in conjunction with this you end up with two trips to read the WSDL file. Note also that you have to create the object as a Fox Object first and this object should match the structure of the server side object exactly.

 

Let’s look at another example that uses an AuthorEntity object to update the Authors from the client. Listing 19 shows the UpdateAuthorEntity method which receives the same AuthorEntity object shown in Listing 14 as a parameter.

 

Listing 19 – Updating the Author from an Object Parameter passed to the Web Service

[WebMethod]

public bool UpdateAuthorEntity(AuthorEntity UpdatedAuthor)

{

      if (UpdatedAuthor == null)

            return false;

 

      busAuthor Author = new busAuthor();

 

      if (!Author.Load(UpdatedAuthor.Au_id)) 

      {

            if (!Author.New())

                  return false;

      }

 

      UpdatedAuthor.SaveRow(Author.DataRow);

 

      if (!Author.Save())

            return false;

 

      return true;

}

 

This code loads the business object based on the Author’s Id. If not found a new Author record is created. Once our author record is loaded – existing or new – we update the data with the data retrieved from the Web Service parameter by saving the contents to the business object’s DataRow member. SaveRow copies the Entity properties to the DataRow fields thus updating the business object. The Business object then can simply save the order.

 

On the FoxPro end we have more work to call this Web Service method. As previously mentioned there’s no built-in mechanism to create an object graph into XML that matches. wwXML does support a method called ObjectToXml but it can only generate element names in lower case since VFP doesn’t support mixed case in property names. So, we’re stuck with manually generating XML in some way. The XML to generate must look as shown in Listing 20.

Listing 20 – Object XML to send to the UpdateAuthorData Web Service Method

<AuthorsEntity>

  <UseColumns>false</UseColumns>

  <PhoneNumbers>

    <HomePhone>808 579-8342</HomePhone>

    <WorkPhone>808 579-8342</WorkPhone>

    <Fax>808 801-1231</Fax>

    <ServiceStarted>1900-01-01T00:00:00.0000000-10:00</ServiceStarted>

  </PhoneNumbers>

  <Au_id>486-29-1789</Au_id>

  <Au_lname>Locksley IV</Au_lname>

  <Au_fname>Charlene</Au_fname>

  <Phone>415 585-4620</Phone>

  <Address>18 Broadway Av.</Address>

  <City>San Francisco</City>

  <State>CA</State>

  <Zip>94130</Zip>

  <Contract>true</Contract>

</AuthorsEntity>

 

To make the Web Service call is shown in Listing 21.

 

Listing 21 – Passing an object to the .Net Web Service using both MSSOAP and wwSOAP

LOCAL loSoap as wwSOAP

loSoap = CREATEOBJECT('wwSoap')

loSoap.Parseservicewsdl(lcWSDL,.T.)  && Parse objects

 

loAuthor = GetAuthorProxyObject()

loAuthor.au_lname = "Strahl"

loAuthor.au_fname = "Rick"

loAuthor.Address = "32 Kaiea Place"

loAuthor.City = "Paia"

loAuthor.State = "HI"

loAUthor.Zip = "96779"

loAuthor.Contract = 0

 

loAuthor.PhoneNumbers.HomePhone = "(808) 579-3121"

loAuthor.PhoneNumbers.ServiceStarted = DateTime()

 

*** Array of Invoices

loAuthor.Invoices[1] = loInv1

loAuthor.Invoices[2] = loInv2

 

lcXML = loSOAP.CreateObjectXmlFromSchema(loAuthor,"AuthorEntity")

 

loDom = CREATEOBJECT("MSXML2.DomDocument")

loDom.LoadXml(lcXML)

 

ShowXml(loDom.DocumentElement.Xml)

 

#IF .T.

loSOAP = CREATEOBJECT("MSSOAP.SoapClient30")

loSOAP.MSSoapInit(lcWSDL)

llResult = loSOAP.UpdateAuthorEntity(loDom.DocumentElement.ChildNodes)

? llResult

? loSOAP.FaultString

RETURN

#ENDIF

 

loSOAP.AddParameter("UpdatedAuthor",loDom.DocumentElement.ChildNodes,"NodeList")

llResult = loSOAP.CallWsdlMethod("UpdateAuthorEntity",lcWSDL)

? loSOAP.cErrorMsg

? llResult

? loSoap.cRequestXml

 

I showed both wwSOAP and MSSOAP in this example. Notice that with wwSOAP you have more options for displaying the XML that was sent and retrieved and you only need to access the WSDL file once.

Error Handling in Web Services

I’ve been mentioning error handling off and on throughout this document and it should be fairly obvious that dealing with data that comes from a Web Service is very different than data that comes from a local source. You should ALWAYS check for errors after a Web Service call.

 

The first thing you should do on the server is to make sure that you capture all Exceptions and wrap them into a SoapException. You’ve seen the code above like this:

 

if (Author.Error)

   throw new SoapException(Author.ErrorMsg,
                           SoapException.ServerFaultCode);

 

Plain exceptions don’t get properly formatted into SOAP exceptions so you might want to capture any Exceptions and rethrow them as SoapExceptions(). Non-SoapException exceptions get thrown back as more complex messages that must be parsed on the client so using SoapException on the server is a must and part of the requirements for WebMethods in general.

 

Avoid this Gotcha:
Here’s a real head scratcher if you don’t know: In your typical ASP. Net debugging environment you might have the following in your web.config file:

 

<configuration>

  <system.web>

    <customErrors mode="RemoteOnly" />

  </system.web>

</configuration>

 

This setting enables detailed error pages to be displayed with ASP.Net pages. It also causes detailed stack trace information to be sent to a Web Service client which makes for less than usable error messages. If you’re debugging Web Services you’ll want to change the value of RemoteOnly to On, which displays ‘friendly’ error messages for ASP.Net errors. For Web Services this means only the immediate Error information is returned to the client, rather than a full stack trace.

 

On the client side the MSSOAP toolkit is pretty messy in picking up Soap errors as it throws a COM exception AND sets a number of properties that parse the error information more cleanly.

 

Luckily in VFP 8 and later this has gotten a lot easier to deal with by simply using a TRY/CATCH block around the code. To review Listing 22 shows typical MSSOAP error handling.

 

Listing 22 – Error Handling with MSSOAP requires Exception blocks

o = CREATEOBJECT("MSSOAP.SoapClient30")

 

loException = null

llError=.f.

TRY

   o.MSSoapInit(lcUrl)

CATCH TO loException

   llError = .t.

ENDTRY

 

IF llError

   ? "Unable to load WSDL file from " + lcUrl

   return

ENDIF

 

TRY

   loNL =  o.GetAuthors("")

CATCH TO loException

   llError = .t.

ENDTRY

 

IF llError

   lcError = loNL.FaultString

   IF EMPTY(lcError)

      lcError = loException.Message

   ENDIF

   ? lcError

   ? loException.ErrorNo

   RETURN

ENDIF

 

*** No we’re ready to something with our result

 

Exception handling should start with the MSSoapInit call. Remember we’re talking about a Web Service here and the MSSoapInit call is going out to the Web to read the WSDL file and then parses it. A lot of things can go wrong here, so don’t just assume this call will succeed.

 

You can capture actual SOAP Method Exceptions  around a call and first check the FaultString which returns a parsed error message from the Web Service. This message gets set even if the SOAP client is throwing an exception so the TRY/CATCH serves more as a handler to ignore the error rather than really catching the Exception.

 

That’s still a lot of code you have to deal with. If you’re using wwSOAP, it makes error handling quite a bit easier as it handles any Web Service errors internally and simply sets a flag you can check as shown in Listing 23.

 

Listing 23 – Error Handling with wwSoap involves checking properties

oSOAP = CREATEOBJECT("wwSOAP")

loAuthor = oSOAP.CallWSDLMethod("GetAuthors",lcWSDL)

 

IF oSOAP.lError

   ? oSOAP.cErrorMessage

   RETURN

ENDIF

 

*** Now we’re ready

? loAuthor.au_id

Using Wrapper Classes to provide Web Service logic

When using Web Services you should treat them like you treat business objects. They aren’t business objects, but they are abstracting a remote application that happens to be accessed over a Web Service. Because you probably will call the Web Service from many locations in your application you probably should create a wrapper that abstracts the usage of the Web Service to a large degree.

 

In particular you should isolate the underlying logic that controls how the Web Service is accessed out of your front end code. Just like the front end should never talk to the database directly in good business object design application, the front end should never talk directly to the SOAP proxy. There’s no reason you should ever have a reference to the MSSOAP or wwSOAP object directly in your front end application code. Instead create a class that wrappers the Web Service and then use that class instead.

 

To help with this process I’ve created a Web Service Proxy generator that creates a class consisting of all the methods of the Web Service. Figure 5 shows this tool in action.

 

Figure 5 – Generating a Visual FoxPro Proxy class that wraps the SOAP client provides Intellisense and abstracts calling the Web Service.

 

This utility creates a wrapper class for the Web Service, a loader and a small block of test code you can run. Once you have created this wrapper class you can very easily call your Web Service like this:

 

DO foxInteropSerivceProxy && Load classes
o = CREATEOBJECT("foxInteropServiceProxy",0)  && wwSOAP

loAuthor = o.GetAuthorEntity("431-12-1221")
IF o.lError
   ? o.cErrorMsg
   RETURN

ENDIF
? loAuthor.au_lname
? loAuthor.PhoneNumbers.WorkPhone

 

If you type the above you’ll also notice that because this is a full Visual FoxPro class you get real Intellisense on it (instead of the hokey Web Service Intellisense through the Web Service Wizard).

 

In this case I’m using wwSOAP and it automatically parsed the object into return value. You can also switch transparently between using wwSOAP and MSSOAP by setting the client mode which is passed with the Init to 1 instead of 0. The code will work the same except that in the example above MSSOAP will return an XML Nodelist instead of the actual object which wwSOAP automatically parsed.

 

The code generated for each method looks something like the code shown in Listing 24.

Listing 24 – The generated client proxy code works with both wwSOAP and MSSOAP

FUNCTION GetAuthorEntity(Id as string) AS variant

LOCAL lvResult

 

DO CASE

*** wwSOAP Client

CASE THIS.nClientMode = 0 

   THIS.SetError()

   THIS.oSOAP.AddParameter("RESET")

 

   THIS.oSOAP.AddParameter("Id",Id)

 

   lvResult=THIS.oSOAP.CallWSDLMethod("GetAuthorEntity",THIS.oSDL)

   IF THIS.oSOAP.lError

      THIS.SetError(THIS.oSOAP.cErrorMsg)

      RETURN .F.

   ENDIF

 

   RETURN lvResult

 

*** MSSOAP Client

CASE THIS.nClientMode = 1 

   LOCAL loException

 

   TRY

      lvResult = THIS.oSOAP.GetAuthorEntity(Id)

   CATCH TO loException

      this.lError = .t.

      this.cErrorMsg = this.oSoap.FaultString

      IF EMPTY(this.cErrorMsg)

         this.cErrorMsg = loException.Message

      ENDIF

   ENDTRY

 

   IF this.lError

      RETURN .F.

   ENDIF

 

   RETURN lvResult

ENDCASE

ENDFUNC

 

As you can see the wrapper methods standardize the method calls and automatically perform the error handling for you so you drastically reduce the amount of error checking code you have to write for each request. The wwSOAPProxy base class also provides easy access properties for setting username and password, proxy configuration options and handles errors for loading the WSDL file.

 

The generated proxy code also contains a section where you can store code that you’ve changed. If you make changes to a specific method you can move the method to this protected area. Then when you regenerate the class the generator preserves the changes – however, it will also generate another copy of your overridden method which you have to delete. Still this goes a long way to let you modify this file.

 

I personally prefer another approach though, which is to create yet another wrapper class for the Web Service. In this subclass I usually perform additional tasks that make it easier to call my Web Service. For example, consider the GetAuthors() method described earlier. This method retrieves a result as a DataSet, but this value is returned to as an XML DOM item rather than the cursor that we really would like to use. So a wrapper class could address this scenario nicely. Listing 25 demonstrates the wrapper method for GetAuthors().

 

Listing 25 – A high level wrapper class should handle things like input and return type conversions

*************************************************************

DEFINE CLASS FoxInteropServiceClient AS FoxInteropServiceProxy

*************************************************************

 

************************************************************************

* foxInteropServiceClient :: GetAuthors

****************************************

***  Function: retrieves all authors into a cursor named Authors

***      Pass: Name

***    Return:

************************************************************************

FUNCTION GetAuthors(Name as string) as Boolean

 

*** Call the base PRoxy

loNL = DODEFAULT(Name)

 

IF this.lError

   RETURN .f.

ENDIF  

 

*** Grab the XML - should be a dataset

IF this.nClientMode = 1

   loDOM = loNL.item(0).parentNode

ELSE

   loDOM = loNL

ENDIF  

lcXML = loDOM.Xml

 

 

LOCAL oXA AS XMLADAPTER

oXA = CREATEOBJECT("XMLAdapter")

oXA.LOADXML(lcXML,.F.,.T.)

 

 

IF (USED("Authors"))

  USE IN AUTHORS

ENDIF

 

oXA.TABLES[1].TOCURSOR(.F.,"Authors")

 

RETURN .t.

 

ENDDEFINE

 

So now we can call this method much more easily in any front end code:

 

o = CREATEOBJECT("foxInteropserviceClient")

llResult = o.GetAuthors("")

IF o.lError

   ? o.cErrorMsg

   RETURN

ENDIF

 

*** Authors table exists now

BROWSE

 

The FoxInterop Client now behaves like any other business component in a typical application. Through this approach we’ve accomplished a lot of functionality:

 

  • Encapsulation
    All logic related to the Web Service is now centralized in 1 top level class.
  • Abstraction
    We’re never talking to the underlying SOAP protocol directly. If we need to update or switch SOAP clients later we can make changes in one place.
  • Error Handling
    The error handling is provided internal to the class so you don’t have to deal with the same few lines of code over and over again.
  • Type conversion
    The wrapper can provide a clean data interface to your application. You’re not passing around XML strings or DOM nodes but rather can input or output cursors or pass or return objects.

 

This is really very similar to a ‘middle tier’ implementation where the Web Service Proxy (MSSOAPClient or wwSoap Client) is the data access layer, and where the ‘wrapper’ class is your business logic layer. Conceptually the wrapper could now be accessed directly or still plug into your business object layer to provide even more abstraction to the front end.

Ready for Service

Wow – a lot of information in this article. Armed with this information you should be ready to tackle your .Net Web Services from Visual FoxPro. Web Services are relatively easy to use and powerful, but it’s important to keep in mind that although they mimic local method calls, they are fundamentally different. The disconnected nature means that data passed over the wire is only a transport and not real objects or cursors. Conversion is required in almost all situations. But armed with good understanding you can pass data in a large variety of ways between the client and server.

 

Which approach is best? Object or DataSet or even arrays or collections of objects? It depends on your application. For Visual FoxPro applications I think that using DataSets and XMLAdapters provide the easiest path of sharing data between the two platforms as this makes it easy for VFP to consume the data in cursor format. The fact that the synchronization mechanism is built into ADO.Net (DataSet)  and VFP both (via XMLAdapter/Table Buffering) makes it easy to pass changed data back and forth. DataSets are expensive in terms of content being shipped over the wire, but for the functionality they provide this may be well worth it.

 

Objects are more light weight but they are a little more difficult to work with from Visual FoxPro and if you do things like update multiple records in tables to send up to the server you will have to manage your own update logic and conflict resolution. This maybe OK – it’s essentially more low level. It’s a good idea to be consistent with your approach, but at the same time remember that you can mix and match when necessary. Use whatever works best for the situation.

 

As always if you have comments or questions, please post them on the West Wind Message Board (www.west-wind.com/wwThreads/) in the White Paper section.

 

Source Code for this article:
http://www.west-wind.com/presentations/FoxProNetWebServices/FoxProNetWebServices.zip

 

West Wind Web Service Proxy Generator Tool:
http://www.west-wind.com/wsdlgenerator/

 

If you find this article useful, consider making a small donation to show your support  for this Web site and its content.

 

By Rick Strahl
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 .NET, Visual Studio and Visual FoxPro. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework and West Wind HTML Help Builder and West Wind Web Store. He's also a C# MVP, a frequent speaker at international developer conferences and a frequent contributor to magazines and books. He is co-publisher of Code magazine. For more information please visit: http://www.west-wind.com/ or contact Rick at rstrahl@west-wind.com.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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