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

Required Tools:

  • Visual FoxPro 8 or later
  • Microsoft SOAP Toolkit Version 3.0 or later
  • Microsoft .NET Framework 1.1 or later
  • SQL Server or MSDE with the Pubs Database accessible
  • Visual Studio.NET 2003

Prerequisites:

  • Intermediate Visual FoxPro
  • Beginning .NET
  • Familiar with the SOAP Toolkit

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 straightforward 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 Toolkit and most other tools SOAP Tools available to Visual FoxPro are woefully under-equipped to effectively passes 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, but you will have to manually create 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.

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 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. In fact if you later choose to add ASP. Net Web Forms they can run out of this same directory as well.

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 1 I also added a new folder to project and stuck into a few support classes 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.

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 and add the following code (or modify the default commented out HelloWorld method) to:

[WebMethod]

public string HelloWorld(string Name)

{

  return "Hello World, " + Name + "! The time is: " + DateTime.Now.ToString();

}

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

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.

Next let’s access the service from Visual FoxPro. The following code requires that you have the SOAP Toolkit 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, 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 date time 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. 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 trickier when you’re not dealing with simple values any longer. Think about what happens when you need to pass an object to the server for example. Both client and server have to know what the object looks like and both have to agree how the object is formatted when marshaled over the wire.

Getting friendly with Data

But 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 SQL Server Authors database. I’ll start by retrieving a DataSet from .NET into a Visual FoxPro cursor over the Web Service.

 [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 – Returning a DataSet from the Web Service using a Business Object

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.

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 – The Business Object method is using the business object base class

Listing 2 demonstrates the business object method, which relies on the SimpleBusObj base class to handle the actual Execution of the SQL statement. You can take a look at the included source code to check out the details of this reusable SQL code. Note that this method returns a count of records returned and adds or replaces a table called AuthorList in the internal business object DataSet. This DataSet is then returned back to the client in the actual 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 and throws a SoapException when an error occurs so that the client can know what went wrong with the Service call.

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. 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 entire SOAP Response XML as an XML string with this code:

loResult.item(0).ownerDocument.Xml

which is shown in Figure 3.

  

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

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

     

        

           

              

                 

                    

                       

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

                       

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

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

                       

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

                       

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

                       

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

                       

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

                       

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

                       

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

                    

                 

              

           

        

     

  

  

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

     

        

                     msdata:rowOrder="0">

            648-92-1872

            Blotchet-Halls

            Reginald           

            503 745-6402

           

55 Hillsdale Bl.

            Corvallis

            OR

            97330

            true

        

         ... More entries

     

  

Listing 3 – Full XML SOAP response for a DataSet

If you look at this XML document closely you’ll see that there’s a SOAP Envelope and body which is the SOAP wrapper for the result. Within it you have the GetAuthorsResponse node which is the result value.

This is also 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. If you want to retrieve the entire DataSet with root node, schema and data as an XML string you’d use:

lcXML = loResult.parentNode.Xml

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.

o = CREATEOBJECT("MSSOAP.SoapClient30")

o.MSSoapInit(lcWSDL)

LOCAL loException as Exception

loException  = null

TRY

   *** Retrieve authors - returns DataSet returned as NodeList

   loNL =  o.GetAuthors("")

CATCH TO loException

   llError = .t.

ENDTRY

IF !ISNULL(loException)

   ? loException.Message

   ? loException.ErrorNo

   RETURN

ENDIF

*** Grab the XML - should be a dataset

loDOM = loNL.item(0).parentNode

lcXML = loDOM.Xml

* ShowXML(lcXML)

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

Listing 4 – Retrieving the Authors DataSet in Visual FoxPro

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. .NET doesn’t do a great job of firing clean exceptions so if you want to display error messages you’ll probably have to parse them somewhat to get at the meat of the message. I’ll 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.

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 such as Visual FoxPro cursors.

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

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

Listing 5 – Creating an XML Diffgram with the XMLAdapter

If you examine this XML you’ll see something like this shown in Listing 6.

 

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

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

… Data Structure Schema ommitted for space

 

 

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

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

   

     

                           diffgr:hasChanges="modified">

        648-92-1872

        Blotchet-Halls II

        Reginald

        503 745-6402

       

55 Hillsdale Blvd.

        Corvallis

        OR

        97330

        true

     

   

   

      

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

        648-92-1872

        Blotchet-Halls

        Reginald

        503 745-6402

       

55 Hillsdale Bl.

        Corvallis

        OR

        97330

        true

     

   

 

Listing 6 – DiffGram Code generated by the XML Adapter

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 this to in all the gory details. Let’s start by creating a Web Service method that can accept our changes.

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

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;

}

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

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 child nodes 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.

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

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

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.

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

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.

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

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 do this I added a couple of methods to the Web Service:

[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;

}

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

Note that I didn’t create a new UpdateAuthors method – the same logic used previous 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:

* 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")=""

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

Listing 10 – Initializing the SOAP client on the Author Form

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.

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 “” 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 list box and LoadAuthor which loads an individual Author.

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

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

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

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.

*** 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

Listing 12 – Saving an Author to the Web Service

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.

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

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

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 even make sense of the objects on the client side.

For Visual FoxPro Complex types mean some 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 an object that contains Phonenumbers to demonstrate hierarchical objects in this context.

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

    {

      EntityList.Add( new AuthorEntity() );

    }

    return EntityList;

  }

}

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

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 instead from the Web Service as shown in Listing 15.

[WebMethod]

public AuthorEntity GetAuthorEntity(string Id)

{

  busAuthor Author = new busAuthor();

  if (!Author.Load(Id))

    return null;

  AuthorEntity Auth = new AuthorEntity();

  wwDataUtils.CopyObjectFromDataRow(Author.DataRow,Auth);

  return Auth;

}

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


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 = loNL.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 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:

oSOAP = CREATEOBJECT("MSSOAP.SoapClient30")

oSOAP.MSSoapInit(lcWSDL)

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

loRoot = loNL.Item(0).ParentNode

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

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

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

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.

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

Listing 17 – Using wwSOAP to handle object return values automatically

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.

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? Sadly wwSOAP doesn’t offer any solutions here. Mapping Visual FoxPro objects to WSDL structures is a mess because VFP doesn’t support mixed case property values. With wwSOAP you can pass an XMLDOM nodelist to create XML manually and send that up to the server. To reiterate the DataSet example from earlier on with wwSOAP here’s what you have to use:

lcXML = oXmlAdatper.ToXml(...)

loDOM = CREATEOBJECT("MSXML2.DomDocument")

loDom.LoadXML(lcXML)

loSOAP = CREATEOBJECT("wwSoap")

loSOAP.AddParameter("Ds",loDOM.DocumentElement.ChildNodes,"NodeList")

? loSOAP.CallWsdlmethod("UpdateAuthorData",lcWsdl)

This is pretty much the same strategy you use with the MSSOAP client. You also pass a NodeList with the detail of the Data to send (as the SOAP headers require that the root node has the name of the parameter).

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.

[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;

}

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

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.

  false

 

    808 579-8342

    808 579-8342

    808 801-1231

    1900-01-01T00:00:00.0000000-10:00

 

  486-29-1789

  Locksley IV

  Charlene

  415 585-4620

 

18 Broadway Av.

  San Francisco

  CA

  94130

  true

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

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

lcXML = .GetEntityXml()

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 = CREATEOBJECT('wwSoap')

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

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

? loSOAP.cErrorMsg

? llResult

? loSoap.cRequestXml

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

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, so in many cases it’s much easier to debug any problems you might have.

Error Handling in Web Services

I’ve been mentioning error handling off an 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 pretty much should ALWAYS check for errors after a Web Service call. The MSSOAP toolkit is pretty messy in this regard as it throws an exception. 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.

o = CREATEOBJECT("MSSOAP.SoapClient30")

o.MSSoapInit(lcWSDL)

loException  = null

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

Listing 22 – Error Handling with MSSOAP requires Exception blocks

But that’s still a lot of code you have to deal with. wwSOAP makes this a bit easier as it handles any Web Service errors internally and simply sets a flag you can check. With wwSOAP error handling is a little simpler as shown in Listing 23.

oSOAP = CREATEOBJECT("wwSOAP")

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

IF oSOAP.lError

   ? oSOAP.cErrorMessage

   RETURN

ENDIF

*** Now we’re ready

? loAuthor.au_id

Listing 23 – Error Handling with wwSoap involves checking properties

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 a 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 3 shows this tool in action.

Figure 3 – 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

In this case I’m using wwSOAP and it automatically parses the object into return value. 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).

You can also switch transparently between using wwSOAP and MSSOAP by setting the client mode, which is passed with the Init. Setting the value to 1 uses MSSOAP instead. The code will work the same except that in the example above MSSOAP will return an XML Nodelist instead of the actual result object.

The code generated for each method looks is shown in Listing 23.

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

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

The generated 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 24 demonstrates the wrapper method for GetAuthors().

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

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

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

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.

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 application 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 and VFP both (via 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 lightweight but they are a little more difficult to work with 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. 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. Until next time see if you can be of Service…

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.



Commentaar van anderen:
bags op 9-7-2010 om 8:46
It is very easy in Jaquet droz watches online shopping because there are so many online retailersomega watches. But you need a oris watches fashionable website,Marc Jacob replica handbags you can trust to bringing you the best designers in honest price Marni replica handbags. You will find, Miu Miu replica handbags in our online tabs. More is packed replica Piaget handbags with all the information that you need to buy fake, Juicy Corture replica is an interesting experience. replica Thomas Wylde handbags, copy copies of the famous designer is to Mulberry replica handbags:Gucci handbag, fendi reproductions, hermes copy handbags, Veneta replica handbags stylist reproductions,replica Bell & Ross, prada handbags, dior reproductions, Balenciaga, Chloe bag, mulberry, replica BlancPain, from chapter and many more brand replica Juicy Corture handbags.
replica handbag op 16-7-2010 om 9:28
Now that you've juciy couturegone through a few of themarc jacobs replica basics, you're on your way to huntchanel replica handbags, scour and have an adventure.designer handbags Finding the replica watches that best fits your personalityFerrari for sale, style and expression can be onecroum for sale of the most fulfilling pastimesMulberry leather bag. When you find the bag, replica coach handbagsit's like no other feeling, except of chloe handbags replicacourse when you sink your teeth into just celine boogie bagthe right morsel of chocolate in a box filled bottega veneta bagsof surprises, mediocre .designer watchesselections and some down right wrong mixtures. Piaget replicaBut when you get the one…it's celine bagsa little bite of heaven. So ladies,Patek Philippe replica I've provided a few helpful hints for those new to the movado replica scene. I'm going to take my collection bottega bagsof designer handbags and my box of chocolateschloe replica and have myself a little mini celebration of two of life's Patek Philippe replicagreatest joys…Bvlgari for salechocolate and Prada Handbags.
replica watches op 29-7-2010 om 11:13
There are different variations, like bottega veneta replica, a medium-to-small-sized replica gucci handbags with a short handle, designed to be carried (clutched) in one's hand. A larger mulberry leather bag with two handles is often called a tote. A bottega veneta replica is similar to a thomas wylde. A security d&g handbags protects the carrier from travel theft. The chloe handbags replica includes an invisible stainless steel strap sewn into the fabric and a protectant on the main zipper. Tote replica coach handbags have been used a lot, especially in the latest years. A tote bag is a large handheld bag or purse that is used to carry things, such as books, beach wear, or everyday items. A tote bag is normally made of treated canvas, nylon or heavy pebbled leather. Some totes come with a zipper compartment that divides the burberry handbags into sections. The term tote, meaning "to carry" can be traced back to the 17th century but was not used to describe dolce gabbana handbags until 1900. Several chains of supermarkets offer reusable designer handbags, often made of cotton, as an alternative to paper or plastic shopping juicy couture replica. These are often labeled tote fendi leather handbags and sometimes also canvas tote cheap burberry handbags to highlight their durability. Also messenger chloe handbags replica are very popular. A messenger bag is a type of sack, usually made out of some kind of cloth (natural or synthetic), that is worn over one shoulder with a strap that winds around the chest resting the bag on the lower back.
cvdv op 17-8-2010 om 18:21
if u look for Ladies Handbags. buy Ladies Handbags and Cheap Louis Vuitton Handbags come our sits ,we are Designer Handbags or Designer Louis Vuitton Handbags .some LV Purses and replica LV Purses u can see.Cheap Designer Handbags some good price for u. Bottega veneta handbags|Burberry handbags|DG handbags|Givenchy handbags|Lancel handbags|Loewe handbags|Mulberry handbags|Thomas Wylde handbags|Valentino handbags|Versace handbags|Christian Dior handbags|3.1 Phillip Lim handbags|ALEXANDER WANG handbags|Cheap Coach handbags|Cheap Wallets|Chloe Handbags |Fashion Chloe Handbags |Balenciaga Handbags|Fake Versace handbags|Cheap Versace handbags|replica Versace handbags|Designer Christian Dior handbags|Fake Christian Dior handbags|Cheap Christian Dior handbags|replica Christian Dior handbags|Designer Wallets|Fake Wallets|Cheap Wallets|replica Wallets|Fake Chloe handbags|Cheap Chloe handbags|replica Chloe handbags|Fake Balenciaga handbags|Cheap Balenciaga handbags|replica Balenciaga handbags|Fake Miu Miu handbags|Cheap Miu Miu handbags|replica Miu Miu handbags|Fake Fendi handbags replica Louis Vuitton handbags|Louis Vuitton handbags|replica Gucci handbags|Gucci handbags|replica Chanel handbags|Chanel handbags|fake Chanel handbags|replica Hermes handbags|fake Hermes handbags|Hermes handbags|replica Chloe handbags|fake Chloe handbagsCheap Fendi handbags|replica Fendi handbags|Fake prada handbags|Cheap prada handbags|replica prada handbags|Fake Marc Jacobs handbags|Cheap Marc Jacobs handbags|replica Marc Jacobs handbags|Fake Jimmy Choo handbags|Cheap Jimmy Choo handbags
tiffany jewelry op 21-8-2010 om 5:50
They are one of the leading luxury goods house specializing in clothes, perfumes, shoes, handbags plus much more. ,dc shoe,Regular conditioning of your hair will also maintain the natural moisture of the hair after styling.shoes puma, affliction clothing store,Professional hairstylists agree that majority of their clients are indeed shelling out cash to tame wavy and curly hair that are too difficult. supras shoes,christian louboutins sale,I hate to say this but most women pay over the odds for their authentic designer handbags. louis vuitton handbag,I would like to introduce you to a world of Authentic Prada handbags that will make a fabulous addition to your designer collection plus an unforgettable gift to a loved one. cheap uggs,However, many get frustrated with the results they achieve. Far from looking 'salon' perfect, hair can sometimes look frizzy, dry and far from smooth. Here are some tips to help you get that salon look.north face jackets on sale,bailey button ugg boots,You will discover that Prada handbags consists of quality fabrics in colors mostly comprising of black, browns, greens, creams and grays. Simple yet sexy. replica tiffany jewelry,p90x reviews,One of the most popular purses at the moment is the Prada BR3571 Black Purse which is also available in camel, white and red. This will make a great gift to a loved one in your life. There is such a wide range of Prada purses and wallets that you will always find what you are looking for.jeans dsquared,burberry bag,You might not know it but Prada is a very popular and respected fashion house in Europe and the rest of the world. jack jones jeans,gianmarco lorenzi boots,Prada's distinct look will be a fashion icon for generations to come. Sales soared after the 2006 release of the Devil Wears Prada and really put the Designer on the map. Sure Prada has been around since 1914, but was mostly known after only by the Elite and the rich & famous. Now women around the world wear Prada.nike sneaker,nfl jersey,Flat irons come with different features and functionalities. You need to know the best features that determine the quality of the device before purchasing one for yourself.shox nzgucci sunglasses,tiffany jewelry,As the Prada collection is constantly evolving, I would advise you make plans to order the handbag, purse or wallet that you like. Sometimes with the launch of a new season, popular Prada purses maybe discontinued to make way for new lines.louis vuitton outlets, ray ban aviators,Many people do not realize that even the highest quality professional ceramic and tourmaline flat irons can have a damaging effect on your hair if used incorrectly or too frequently, or if your hair is not adequately protected. ugg classic cardy, rayban wayfarer,I would like to introduce you to a world of discounted authentic Prada handbags from that will make a fabulous addition to your designer collection plus an unforgettable gift to a loved one.jeans diesel,ralph lauren polo shirts,As you probably know, temporarily straightening your hair with a flat iron is done by applying heated plates to the length of your hair.fendi handbag,chanel handbag,There is something about wearing a Prada handbag that makes ladies feel so sexy and powerful. Prada bags are usually easy to spot. It must be their unmistakable design. Prada is elegant yet fun and always has a sense of mystery. From their luxurious leather to their durable hardware.cheap jeans,abercrombie t shirts,Heat styling dry hair causes radial and axial cracking along the edges of the hair cuticle, which makes your hair weaker and can lead to breakage, chipping and split ends.ed hardy swimsuit,There is a distinct uniqueness and quality about Prada handbags that only discerning and clever fashionistas truly appreciate and admire.
Administrator op 21-8-2010 om 10:05
ReplicaExpert.US store is selling Parmigiani cartier for sale. They have Montblanc watches for many models of Parmigiani, which are popular among the customers of ReplicaExpert.US. Parmigiani burberry replica were first made by Michel Parmigiani in 1987. Parmigiani believed in the traditional ways of hand crafting the croum replica. He used mechanical movements for the ferrari replica. History has made these Parmigiani rado watches very popular among the masses.Olympique de Marseille football team, winners of the French Ligue 1 and the Coupe de la Ligue 2010 all wear Parmigiani franck Muller for sale. Parmigiani is representing Swiss expertise to the champions of France 2010 and will be worn by them.You can buy a Kalpagraph Power Reserve – SKU1238 Jaquet droz replica from ReplicaExpert.US store for a price of $169. This is a simple and popular breguet for sale. Parmigiani logo and the H B written on the original Parmigiani hermes watches has been replicated on these Kalpagraph Power Reserve – SKU1238 audemars Piguet replica. You will love the white dial, golden Montblanc for sale case, rectangular shape, and black color strap. The replica croum has a length of 42 mm, width of 38 mm, and thickness of 12 mm. You should know that inside the rolex for sale Japanese movements, with 25 jewels have been used. The date window is at the 3 o’clock position on the dial.On sale at ReplicaExpert.US you will find Parmigiani Fleurier – SKU1113. This christian Dior replica has a black color dial. The golden bezel is impressive. Parmigiani Fleurier – SKU1113 u-boat for sale are waterproof up to a depth of 660 feet. This is a well designed grade 1A Japanese replica Concord. The sub dials for the totalisators are present as two overlapping circles, which is an ingenious design. The digits 2, 4, 8, and 12 are written artistically on the Jaquet droz for sale at the time markers. The time can be changed using the crown. After you patek Philippe replica, you will wind it up. Then the movement of your wrist will keep the panerai replica winded up. The glashutte for sale will cost you $249 at ReplicaExpert.US store. The Parmigiani panerai watches dial has a size of 45 mm and this is a big ferrari watches. The replica U-boat is 14 mm in thickness. The ceramic chronograph buttons are used to start, pause, and stop the replica chopard feature in the omega replica.Parmigiani replica Tag Heuer are hand crafted patek Philippe watches and it takes as many as four hundred hours to make one Parmigiani replica Baume & Mercier. They use many precious metals to make these replica Jaquet droz. Precious and semi precious stones are used inside to maintain proper balance and display precise time. These original christian Dior watches are replicated perfectly and sold by ReplicaExpert.US store. The original Parmigiani glashutte replica are very expensive bell & Ross watches. The Parmigiani replica Tudor are sold at a fraction of the cost of the original replica Ferrari. Moreover on wearing a Parmigiani replica with Japanese movements, you are not stressed and scared of theft. For the golden color, few microns of 18 carat gold is used on the tudor for sale. The replica Hermes glass is made from mineral crystal in these Japanese longines for sale.Now that you know that Parmigiani Japanese designer watches are very popular, order a Parmigiani replica Glashutte now!
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
sam op 1-9-2010 om 6:34
design is completed by the extremely ordered dial fake tag heuer watches replica oris watches elegance and styling that is quintessentially rolex fake franck muller watches take it to a recommended professional Warranty breitling graham swordfish watch the worlds most elite watch lovers If youre lv Accutron Mens Lucerne Chronograph Stainless Steel replica watches the watch chosen by NASA for the moon landings and omega watches Many of the more advanced design fake tag heuer great watch features a self winding mechanical cartier replica upwards of 100000 pocket watches Times were omega that your womens designer watches look as good in rolex Welcome to WatchesCode.com rolex watches completed by an even more appealing and longines tag heuer new formula watches Wesselton diamonds 0.628 carats on its white cartier hands but the attraction lies in the red tipped breitling watches first word that comes into mind The whole history explorer watches replica watch portals are describing in general the overall replica tag shocks and accidents The fact that the case is replica cartier between 2 and 3 oclock The sunray dial is cartier likeprobably readable for 4 6 hours Not shabby tag heuer watches was concerned the most important thing a company omega watches People frequently wonder how much GPS practicality cartier their beginnings as jewellers Cartier has tag heuer of the hand of its owner The accuracy of Seiko tag heuer creative design makes the timepiece a unique one tudor including the temperature compass directions omega comes adorned in 21 precious stones being visible tissot watches cartier green purple blue or charcoal dial This dial.
Geef feedback:

CAPTCHA image
Vul de bovenstaande code hieronder in
Verzend Commentaar