Improve VFP data retrieval with WCF

With the WCF client we can download the data from the server in asynchronous (non blocking) mode. The measurement in this article show us this can be done much faster then downloading the data with VFP functions or oledb data adapters.

Abstract

Windows Communication Foundation (WCF) enables us to create distributed architecture and use Visual FoxPro (VFP) as a client/server system executing the operation on the server (backend). With the WCF client we can download the data from the server in asynchronous (no blocking) mode. The measurement in this article show us this can be done much faster then downloading the data with VFP functions or oledb data adapters. With WCF, we can take advantage of parallel (asynchronous) operation and distribute the processing of data between the client and the server. The retrieved data can be presented in VFP or Net (Windows) controls. In the download of this article is the code of four projects: WCF service, WCF client, COM object and VFP client. We present the code and the measurement of retrieving the data from the server in the form of a data table, a dataset, a strong typed collection and the dbf file. The fastest is the retrieve and transport of the dbf file we bind to the VFP grid, on the second place (24%slower) is the strong typed Net collection we bind to the Net grid, the laziest (30% slower) is the Net data table we bind to the windows Net grid. Except for very small record set, there is a big time gap (almost 100%) between the direct VFP data retrieve over the local network and if we use the WCF service. 

The problem

For example, from the table of size 400 MB let us download six thousand records (four fields of total length less than 50 characters) from the VFP database on the server. The first retrieve of this records set at the work station can exceed 12 seconds (at the server only 1 second [6]]). The workstation on which is running the client block for 12 seconds. If we download consequently two records set, we block the VFP form for 24 seconds and so on. Can we retrieve two thousand records large records set in few seconds without blocking the client?

The solution

On the first retrieve of data from the server the VFP client, download also the relatively large index file and dbf table headers. Using the WCF, we can retrieve the data on the server (backend) and download only the selected records. We can execute asynchronously the client operations (in parallel with other VFP operation). This may be the way to go if you have a VFP application running long VFP requests. You create and install:

- WCF service. (How to create a WCF in Visual Studio 2008? see. [2]).We shall use the net.tcp protocol.

- Host the WCF service. In our projects, we shall host it in Windows service. Create a Windows service and install it on the server.

- Create the WCF client (How to do this? See [1], [3]).

- Create the Windows control (Active-X) and makes it COM visible (see [1]). Register it on the workstation.

- Create the VFP project, the VFP form and add it the created Active-X control. Run the VFP application on the workstation.

The components of the above projects are interconnected. We pass the data (objects) between these components. We shall pay special attention to the methods and objects we use for this intra components transport.

The WCF service

The main goal of the WCF service is to accept the requests from the client and pass the requests to the VFP backend, catch the response from the VFP database application (on the server) and pass the response to the client. We shall create the WCF service with a project named WfcVfpWsTcp and six files:

1.       service.cs (Listing 1). This file contain the interface IService1 with 6 operation which allows us to tansport the Net generic data types: String, DataSet, DataTable, Byte  and a generated data type OrderListCollection.   In the background the WCF serialize this five objects. In all the methods we have the out parameter retError. With it, we send to the client the error message generated on the server (service) side. The service operation methods accept the names and parameters of the method it will execute in the VFP database and route it to the VFP database by calling the VfpServerCon. Normally the service WfcVfpWsTcp is on the same computer as the VFP server.

Listing 1. The service.cs class

using System;

using System.Reflection;

using System.Collections.Generic;

using System.Text;

using System.ServiceModel;

using System.Runtime.Serialization;

using System.IO;

using System.Configuration;

using System.Data;

using System.Collections;

using System.ServiceModel.Channels;

namespace WfcVfpWsTcp

{

    [ServiceContract]

    [ServiceKnownType(typeof(Orders))]

       public interface IService1

    {

        [OperationContract]

        string MyOperation1(out string retValue,string myValue);

        [OperationContract]

        byte[] StoreHit(out string retError,string RequestMethod, string paramFrom,string paramTo,string param3);

        [OperationContract]

        byte[] StoreFile(out string retError,string RequestMethod, string paramFrom, string paramTo);

        [OperationContract]

        byte[]  StoreDataSet(out string retError,string RequestMethod, string paramFrom, string paramTo);

        [OperationContract]

        DataTable StoreDataTable(out string retError, string RequestMethod, string paramFrom, string paramTo);

        [OperationContract]

        OrderListCollection StoreCollection (out string retError,string RequestMethod, string paramFrom, string paramTo);

    }

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, IncludeExceptionDetailInFaults = true)]

    public class service1 : IService1

    {

        private string _filePath = ConfigurationManager.AppSettings["filePath"];

        VfpServerCon VFPSer = new VfpServerCon();

        public string MyOperation1(out string retValue,string myValue)

        {

            OperationContext context = OperationContext.Current;

            MessageProperties messageProperties = context.IncomingMessageProperties;

            RemoteEndpointMessageProperty endpointProperty =messageProperties[RemoteEndpointMessageProperty.Name]  as RemoteEndpointMessageProperty;

            retValue = "From service-error:" + myValue;

            return string.Format("Hello {0}! Your IP address is {1} and your port is {2}",myValue , endpointProperty.Address, endpointProperty.Port);

        }

        public DataTable StoreDataTable(out string retError, string MethodName, string paramFrom, string paramTo)

        {

            DataTable retds = this.VFPSer.StoreDataTable(out  retError, MethodName, paramFrom, paramTo);

            retError = this.VFPSer.Merror;

            return retds;

        }

        public byte[] StoreDataSet(out string retError, string MethodName, string paramFrom, string paramTo)

        {

          byte[]     retds = this.VFPSer.StoreDataSet(out  retError,MethodName, paramFrom, paramTo);

          retError = this.VFPSer.Merror;

          return retds;

        }

        public byte[] StoreHit(out string retError, string MethodName, string paramFrom, string paramTo, string param3)

        {

            byte[] retValue = this.VFPSer.StoreFile(out  retError, MethodName, paramFrom, paramTo);

            retError = this.VFPSer.Merror;

            return retValue;

        }

        public byte[] StoreFile(out string retError, string MethodName, string paramFrom, string paramTo)

        {

           byte[] retValue = this.VFPSer.StoreFile(out  retError,MethodName, paramFrom, paramTo);

           retError = this.VFPSer.Merror;

            return retValue;

        }

        public OrderListCollection StoreCollection(out string retError,string MethodName, string paramFrom, string paramTo)

        {

          OrderListCollection orl= this.VFPSer.StoreCollection (out retError,MethodName, paramFrom, paramTo);

          retError = retError;

          return orl;

        }

           /// <summary>

        /// Disposes the current instance.

        /// </summary>

        private string _errorMessage = "";

        public string ErrorMessage

        {

            get { return _errorMessage; }

            set { _errorMessage = value; }

        }

    }

}

2.       VfpServerCon.cs (Listing 2) (Its main goal is to retrieve the data from the VFP database using the OleDb driver).  It accepts the names and parameters of the method it will execute in the VFP database, accept the response from the VFP (normally as the OleDbDataReader), transform the data (for example in byte array) and pass the data to the service. (You can retrieve the data from the VFP database also with the VFP COM object (see [3])).

Listing 2. The VfpServerCon.cs class

using System;

using System.Data;

using System.Configuration;

using System.Web;

using System.Diagnostics;

using System.Data.OleDb;

using System.IO;

using System.Xml;

using System.Collections;

using System.Text;

    /// <summary>

    /// Summary description for VfpServerO

    /// </summary>

    ///

    public class VfpServerCon

    {

        private string conStr = System.Configuration.ConfigurationManager.AppSettings["connStr"];

        private string filePath = System.Configuration.ConfigurationManager.AppSettings["filePath"];

        System.Data.OleDb.OleDbConnection conn;

        private string _merror;

        EventLog Log = null;

        public VfpServerCon()

        {

            conn = new System.Data.OleDb.OleDbConnection(this.conStr);

        }

        public string Test(string MethodName, string RequestParameter)

        {

            return MethodName + RequestParameter;

        }

 

 

        public DataTable StoreDataTable(out string retError,string MethodName, string RequestParameter1, string RequestParameter2)

        {

          

            DataTable dt0 = new DataTable();

            dt0.TableName = "Order";

            OleDbCommand cmds = this.OpenCon(MethodName, RequestParameter1, RequestParameter2);

            if (this.Merror != string.Empty)

            {

                retError = this.Merror;

                return null;

            }

            System.Data.OleDb.OleDbDataReader dr = null;

           try

            {

                dr = cmds.ExecuteReader();

               dt0.Load(dr, LoadOption.OverwriteChanges);

            }

            catch (Exception ex)

            {

                this.Merror = ex.Message.ToString() + "Can not load dataTable";

                retError = this.Merror;

                return null;

            }

            dr.Dispose();

            conn.Close();

           retError = "";

            return dt0;

        }

        public byte[] StoreDataSet(out string retError, string MethodName, string RequestParameter1, string RequestParameter2)

        {

            DataSet ds1 = new DataSet();

            DataTable dt0 = new DataTable();

            dt0.TableName = "Order";

            ds1.Tables.Add(dt0);

           OleDbCommand cmds = this.OpenCon(MethodName, RequestParameter1, RequestParameter2);

            if (this.Merror != string.Empty)

            {

                retError = this.Merror;

                return null;

            }

            System.Data.OleDb.OleDbDataReader dr = null;

            try

            {

                dr = cmds.ExecuteReader();

                ds1.Load(dr, LoadOption.OverwriteChanges, ds1.Tables[0]);

            }

            catch (Exception ex)

            {

                this.Merror = ex.Message.ToString() + "Can not load dataset";

                retError = this.Merror;

                return null;

            }

            dr.Dispose();

            conn.Close();

            ds1.RemotingFormat = SerializationFormat.Binary;

            MemoryStream ms = new MemoryStream();

            ds1.WriteXml(ms, XmlWriteMode.WriteSchema);

            ms.Position = 0;

            Byte[] buff = new byte[ms.Length];

            ms.Read(buff, 0, buff.Length);

            ms.Close();

            ms.Dispose();

            retError = "";

            return buff;

        }  

        public byte[] StoreFile(out string retError,string MethodName, string RequestParameter1, string RequestParameter2)

        {

            OleDbCommand cmds = this.OpenCon(MethodName, RequestParameter1, RequestParameter2);

            if (this.Merror != string.Empty)

            {

                retError = this.Merror;

                return null;

            }

            System.Data.OleDb.OleDbDataReader dr = null;

            string rez = "";

            try

            {

                dr = cmds.ExecuteReader();

                dr.Read();

                rez = dr[0].ToString(); // return string filePath

            }

            catch (Exception ex)

            {

                this.Merror = "From storefile empty string:" + ex.Message + "  " + filePath;

                retError = this.Merror;

                return Encoding.UTF8.GetBytes(this.Merror);

            }

            dr.Dispose();

            conn.Close();

            if (rez != string.Empty) //Return file

            {

                rez = @filePath + rez;

            }

            else

            {

                this.Merror = "From storefile empty string:" + "  " + filePath;

                retError = "";

                return Encoding.UTF8.GetBytes(this.Merror);

            }

            byte[] buffer = null;

            try

            {

                FileStream fs = new FileStream(@rez, FileMode.Open, FileAccess.Read);

                buffer = new byte[fs.Length];

                fs.Read(buffer, 0, (int)fs.Length);

                fs.Close();

                FileSystemInfo fsi = new FileInfo(@rez);

                fsi.Delete();

            }

            catch (Exception ex)

            {

                this.Merror = "From storefile:" + ex.Message.ToString() + "  " + rez;

                retError = this.Merror;

                return Encoding.UTF8.GetBytes(this.Merror);

            }

            retError = "";

            return buffer;

        }

        public string Merror

        {

            get

            {

                return _merror;

            }

            set

            {

                _merror = value;

            }

        }

        public OleDbCommand OpenCon(string MethodName, string RequestParameter1, string RequestParameter2)

        {

            System.Data.OleDb.OleDbCommand cmd = new System.Data.OleDb.OleDbCommand("select * from pgp", conn); //Open data

            string metpar = MethodName + "('" + RequestParameter1 + "','" + RequestParameter2 + "')";

            System.Data.OleDb.OleDbCommand cmds = new System.Data.OleDb.OleDbCommand(metpar, conn);

            cmds.CommandType = CommandType.StoredProcedure;

            this.Merror = string.Empty;

            try

            {

                conn.Open();

            }

            catch (Exception ex)

            {

                this.Merror = ex.Message.ToString() + " Can not open conn.";

 

                return cmds;

            }

            try

            {

                cmd.ExecuteScalar();

            }

            catch (Exception ex)

            {

                this.Merror = ex.Message.ToString() + " Can not read " + MethodName + " " + RequestParameter1 + "  " + conn;

                return cmds;

            }

            cmd.Dispose();

            return cmds;

        }

        public OrderListCollection  StoreCollection(out string  retError,string MethodName, string RequestParameter1, string RequestParameter2)

        {

            OrderListCollection arl = new OrderListCollection();

            byte[] buf = null;

            OleDbCommand cmds = this.OpenCon(MethodName, RequestParameter1, RequestParameter2);

            if (this.Merror != string.Empty)

            {

                retError= this.Merror;

                return arl;

            }

            System.Data.OleDb.OleDbDataReader dr = null;

            try

            {

                dr = cmds.ExecuteReader();

                Orders  order = null;

                while (dr.Read())

                {

                    order = new Orders();

                    order.Orderid = (Decimal)dr[0];

                    order.Doctype = dr["doctype"].ToString();

                    order.Wareid = dr["wareid"].ToString();

                    order.Orderdate = (DateTime)dr["orderdate"];

                    arl.Add(order);

                }

            }

            catch (Exception ex)

            {

                this.Merror = ex.Message.ToString() + " Can not execute " + MethodName + " " + RequestParameter1 + "  " + conn;

                retError = this.Merror;

                return arl;

            }

            dr.Dispose();

            conn.Close();

            retError = "";

            return arl;

        }

    }

When we initialize the VfpServerCon class, it read the connection string from the app.config (see the key connString in app.confing).

You have to write your own connection string in the app.config and write your own store procedures. In Listing 2 [6] is the store procedure used in this article. It accepts two parameters and returns a result set. With small modification, this procedure can write the results in the dbf file and return a string with its name. We suppose the last method is the fastest retrieve method. Transporting a dbf file is relatively unusual in WCF project, so we describe this process in more detail.

When we call the StoreFile method of the VfpServerCon instance (Listing 2), the method StoreFile is executed. The VFP database method StoreFile retrieves the data from the database, copy it in the dbf file and pass its name back to the calling method. It read the dbf file from the disk and writes it in the byte buffer (Listing 2). The program passes this buffer to the service, which sends it to the client (Listing 1).

Listing 3. The OrderListCollection class

using System;

using System.Collections.Generic;

using System.Text;

using System.Collections;

using System.ServiceModel;

using System.Runtime.Serialization;

    /// <summary>

    /// Summary description for OrderListCollection

    /// </summary>

    ///

    [Serializable]

    [DataContract]

    public class Orders : IComparable

    {

        private decimal _orderid;

        private string _wareid;

        private DateTime _orderdate;

        private string _doctype;

        [DataMember]

        public decimal Orderid

        {

            get { return _orderid; }

            set { _orderid = value; }

        }

        [DataMember]

        public string Wareid

        {

            get { return _wareid; }

            set { _wareid = value; }

        }

        [DataMember]

        public string Doctype

        {

            get { return _doctype; }

            set { _doctype = value; }

        }

        [DataMember]

        public DateTime Orderdate

        {

            get { return _orderdate; }

            set { _orderdate = value; }

        }

        public Orders()

        {

            _orderdate = DateTime.Now;

            _doctype = string.Empty;

            _wareid = string.Empty;

            _orderid = 0;

        }

        public Orders(decimal orderid, DateTime orderdate, string wareid, string doctype)

        {

            _orderdate = orderdate;

            _doctype = doctype;

            _wareid = wareid;

            _orderid = orderid;

        }

        public int CompareTo(object obj)

        {

            if (!(obj is Orders))

            {

                throw new ArgumentException("Object provided is of the wrong type");

            }

            Orders ord = (Orders)obj;

            int cmpl = this.Orderid.CompareTo(ord.Orderid);

            if (!(cmpl == 0))

            {

                return cmpl;

            }

            return this.Orderid.CompareTo(ord.Orderid);

        }

    }

    [Serializable]

    [CollectionDataContract]

    public class OrderListCollection : CollectionBase

    {

 

        public Orders this[int idx]

        {

            get

            {

                return (Orders)this.InnerList[idx];

            }

 

        }

        public void Add(Orders ord)

        {

            this.InnerList.Add(ord);

        }

    }

 

3.       OrderListCollection.cs (Listing 3). We frame the elements of this class with DataContrat, DataMember, CollectionDataContract and Serializable attributes. Do not forget to add to the service.cs the attribute [ServiceKnownType(typeof(Orders))].  Decorated with this attributes the new data type can be serialized with the WCF serialization tools. If you change any of this attribute, you have to update the client with the new service metadata.

 

Listing 4. The app.config file

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

  <appSettings>

    <add key="connStr" value="Provider=VFPOLEDB.1;Data Source=d:\moj\ateks2006\data;Collating Sequence=MACHINE"/>

    <add key="VFPServerName" value="vfpserver.ComData"/>

    <add key="filePath" value="d:\moj\ateks2006\"/>

  </appSettings>

 

  <system.serviceModel>

    <bindings>

      <netTcpBinding>

        <binding name="NetTcpBinding" transferMode="Buffered" maxReceivedMessageSize="5242880">

          <security mode="None" />

        </binding>

      </netTcpBinding>

    </bindings>

    <diagnostics>

      <messageLogging logEntireMessage="true" logMalformedMessages="true"

        logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="true"

        maxMessagesToLog="1000" />

    </diagnostics>

    <behaviors>

      <serviceBehaviors>

        <behavior name="mex">

          <serviceDebug includeExceptionDetailInFaults="true" />

           <serviceMetadata />

        </behavior>

      </serviceBehaviors>

    </behaviors>

    <services>

      <service name="WfcVfpWsTcp.service1" behaviorConfiguration="mex">

        <host>

          <baseAddresses>

            <add baseAddress="net.tcp://192.168.123.63:8081/wcfvfpwstcp" />

          </baseAddresses>

        </host>

        <endpoint  binding="netTcpBinding"

          bindingConfiguration="NetTcpBinding" name="NetTcpEndPoint" contract="WfcVfpWsTcp.IService1" />

        <!-- metadata exchange (MEX) endpoint -->

        <endpoint address="mex" binding="mexTcpBinding"  name="MEX" contract="IMetadataExchange" />

      </service>

    </services>

  </system.serviceModel> </configuration>

4.       app.config (Listing 4).  In the app.config we put a net.tcp binding, its endpoints and the corresponding mex binding, which allow us to download the service description to the client.

We use the key filePath (Listing 4) in the method StoreFile (Listing 2). It passes to the VFP database method the path of the file written on the disk.

Do not forget to change this filePath to your database path. Change the base endpoint address (192.168.123.63) to the IP of your computer, where the service is installed.

Listing 5. The HostServiceVfp file

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Diagnostics;

using System.Linq;

using System.ServiceProcess;

using System.ServiceModel;

using System.Text;

namespace WfcVfpWsTcp

{

    public partial class HostServiceVfp : ServiceBase

    {

        public ServiceHost serviceHost = null;

        public HostServiceVfp()

        {

            InitializeComponent();

            this.ServiceName = "WcfVfpWsTcp";

            this.AutoLog = true;

            this.CanShutdown = false;

            this.CanStop = true;

            this.CanPauseAndContinue = false;

         }

        protected override void OnStart(string[] args)

        {

            if (serviceHost != null)

            {

                serviceHost.Close();

            }

            serviceHost = new ServiceHost(typeof(service1));

            serviceHost.Faulted += new EventHandler(Host_Faulted);

            // Open the ServiceHostBase to create listeners and start

            // listening for messages.

            serviceHost.Open();

        }

        protected override void OnStop()

        {

            if (serviceHost != null)

            {

                serviceHost.Close();

                this.EventLog.WriteEntry("ServiceHost has been closed.",

                EventLogEntryType.Information, 0, 0);

                serviceHost = null;

            }

        }

        void Host_Faulted(object sender, EventArgs e)

        {

            this.EventLog.WriteEntry("ServiceHost has faulted.", EventLogEntryType.Error, 0, 0);

        }

    }

}

5.       HostServiceVfp.cs (Listing 5). The class to start and stop the service. In case of a fault of the service, it writes the error message to the log file.

6.       ProjectInstaller.cs. Enable the installation of the Windows service and make it visible in the MMC (Administrative Tolls – Services).(See [5])

Create the setup project WfcVfpWsTcpIn (see [4] )

The WCF client

- Create a new project named ComWfcVfpClient (See [4]). Rename the created class to ComWfcVfpClient.cs (Listing 6).

 

Listing 6. The WCF Client class

using System;

using System.Collections.Generic;

using System.Text;

using System.ServiceModel;

using System.ServiceModel.Channels;

using System.Configuration;

using System.Threading;

using System.IO;

using System.Runtime.InteropServices;

using System.Data;

using System.Xml;

namespace ComWfcVfpClient

{

    public class WcfVfpClient

    {

        public delegate void GridEventHandler(object source,EventArgs e);

        public event GridEventHandler ReceivedData;

        private ComEventArgs eventArgs;

        private string endPointUri = System.Configuration.ConfigurationManager.AppSettings["uri"];

        private ComWfcVfpClient.WcfVfpWs.OrderListCollection olc = null;

        public void OnReceivedData(object source, EventArgs e)

        {

            if (ReceivedData != null)

            {

                ReceivedData(source, e);

            }

        }

        public void StoreResultCom( object source, string methodName, string paramFrom, string paramTo)

        {

             eventArgs = new ComEventArgs();

            eventArgs.ReceivedState = source;

            string Output = null;

           //// Proxy = null; //if you run a new instace of the proxy (open a new channel) in case of parallel call

         //   System.Windows.Forms.MessageBox.Show("start the proxy store result com");

            if (!this.ProxyNotStarted()) return;

           string retError = null;

            try

            {

                 olc =(ComWfcVfpClient.WcfVfpWs.OrderListCollection) Proxy.StoreCollection(out retError,methodName, paramFrom, paramTo);

                 if (retError != string.Empty)

                 {

                     eventArgs.ErrorMessage = "Error received StoreResultCom:  " + retError;

                }

                 else

                 {

                     eventArgs.ReceivedType = "C";

                     eventArgs.ReceivedCollection = olc; //remember it is subscribed to the delegate notification

                 //    System.Windows.Forms.MessageBox.Show("Cllection string length:" + olc.ToString().Length.ToString());

                 }

              this.ReceivedData(this, eventArgs);

     //           Log.WriteEntry("Return dt from storeResultcom:" + methodName + "  " + paramFrom + paramTo );

            }

            catch (Exception ex)

            {

                this.ReturnError( ex.Message );

            }

           //  ((IClientChannel)Proxy).Close();

           this.Dispose();

            return ;

        }

        public void StoreResultComAsyn(object source, string methodName, string paramFrom, string paramTo)

        {

            ThreadStart starter = delegate { this.StoreResultCom(source, methodName, paramFrom, paramTo); };

            new Thread(starter).Start();

            //anonymus method

        }

        public String MyOperation1(out string retValue, string myValue)

        {

            string Output = null;

            ComEventArgs eventArgs = new ComEventArgs();

            if (!this.ProxyNotStarted())

            {

                retValue = eventArgs.ErrorMessage;

                this.ReturnError(retValue);

                return retValue;

            }

         try

            {

                    Output = this.Proxy.MyOperation1(out retValue, "hello");

 Output = "Rv: " + retValue+  OperationContext.Current.IncomingMessageHeaders.Count.ToString() + OperationContext.Current.IncomingMessageHeaders.MessageId + " " + Output;

            }

            catch (Exception ex)

            {

                this.Dispose();

                this.ErrorMessage = "MyOperaton1Com:" + ex.Message;

                retValue = this.ErrorMessage;

                this.ReturnError(retValue);

                return this.ErrorMessage;

            }

           Output =this.Proxy .MyOperation1(out retValue, "hello");

         //  ((IClientChannel)Proxy).Close();

            this.Dispose();

             return Output;

        }

        public void StoreHitCom(object source, string methodName, string paramFrom, string paramTo)

        {

            string Output = null;

            string retError = null;

            eventArgs = new ComEventArgs();

            eventArgs.ReceivedState = source;

            eventArgs.ReceivedType = "S";

            if (!this.ProxyNotStarted()) return;

            try

            {

                Output = Encoding.UTF8.GetString(Proxy.StoreHit(out retError ,methodName, paramFrom, paramTo, ""));

                if (retError != string.Empty)

                {

                    this.ReturnError(retError);

                }

                else

                {

                   eventArgs.ReceivedString = Output;

                }

            }

            catch (Exception ex)

            {

              retError = "StoreHitCom:" + ex.Message;

              this.ReturnError(retError);

            }

            this.ReceivedData(this, eventArgs);

            this.Dispose();

        }

        public void StoreDataSet(object source, string methodName, string paramFrom, string paramTo)

        {

            string Output = null;

            string retError = null;

            eventArgs = new ComEventArgs();

            eventArgs.ReceivedState = source;

            if (!this.ProxyNotStarted()) return;

            try

            {

             //   System.Windows.Forms.MessageBox.Show("vfp start");

                byte[] xout = Proxy.StoreDataSet(out retError, methodName, paramFrom, paramTo);

                DataSet ds = new DataSet();

           //     System.Windows.Forms.MessageBox.Show("Dataset byte length:"+xout.Length .ToString());

                if (retError != string.Empty)

                {

                   eventArgs.ErrorMessage = retError;

                }

                else

                {

                    Output = Encoding.UTF8.GetString(xout);

                    MemoryStream stream = new MemoryStream(xout);

                    XmlTextReader reader = new XmlTextReader(stream);

                    ds.ReadXml(reader);

                    eventArgs.ReceivedType = "D";

                    eventArgs.ReceivedDataSet =ds ;

                }

            }

            catch (Exception ex)

            {

                this.ErrorMessage = "StoreDataSet:" + ex.Message;

                eventArgs.ErrorMessage = this.ErrorMessage;

            }

            ReceivedData(this, eventArgs); //publish the event

            this.Dispose();

        }

        public void StoreDataTable(object source, string methodName, string paramFrom, string paramTo)

        {

            string Output = null;

            string retError = null;

            eventArgs = new ComEventArgs();

            eventArgs.ReceivedState = source;

         if (  ! this.ProxyNotStarted()) return;

            try

            {

                //   System.Windows.Forms.MessageBox.Show("vfp start");

                DataTable  xout = Proxy.StoreDataTable(out retError, methodName, paramFrom, paramTo);

                  //        System.Windows.Forms.MessageBox.Show("DatTable rows count:" + xout.Rows.Count. ToString());

                if (retError != string.Empty)

                {

                    eventArgs.ErrorMessage = retError;

                }

                else

                {

                    eventArgs.ReceivedType = "T";

                    eventArgs.ReceivedDataTable = xout;

                }

            }

            catch (Exception ex)

            {

                this.ErrorMessage = "StoreDataTable:" + ex.Message;

                eventArgs.ErrorMessage = this.ErrorMessage;

            }

            ReceivedData(this, eventArgs); //publish the event

            this.Dispose();

        }

        public void StoreHitComAsyn(object source, string methodName, string paramFrom, string paramTo)

        {

            ThreadStart starter = delegate { this.StoreHitCom(source, methodName, paramFrom, paramTo); };

            new Thread(starter).Start();

        }

        public void StoreDataSetAsyn(object source, string methodName, string paramFrom, string paramTo)

        {

            ThreadStart starter = delegate { this.StoreDataSet(source, methodName, paramFrom, paramTo); };

            new Thread(starter).Start();

        }

        public void StoreDataTableAsyn(object source, string methodName, string paramFrom, string paramTo)

        {

            ThreadStart starter = delegate { this.StoreDataTable(source, methodName, paramFrom, paramTo); };

            new Thread(starter).Start();

        }

        public void StoreFileCom(object source, string methodName, string paramFrom, string paramTo, string filePath)

        {

         //   Log.WriteEntry("start com storefilecom");

              eventArgs = new ComEventArgs();

            eventArgs.ReceivedType = "F";

            eventArgs.ReceivedState = source;

            string retError = null;

            if (!this.ProxyNotStarted()) return;

            try

            {

           //     Log.WriteEntry("start com storefilecom:" + methodName + "  " + paramFrom + paramTo + " "+ filePath);

                byte[] Received = Proxy.StoreFile(out retError, methodName, paramFrom, paramTo);

               // Log.WriteEntry("Received com storefiletcom:" + methodName + "  " + paramFrom + paramTo + " " + Received.Length.ToString() + " " + filePath);

              if (retError != string.Empty)

                {

                    this.ReturnError(retError);

                 }

                else

                {

                    if (filePath == string.Empty) filePath = @"D:\moj\vajawcf\aaa.dbf";

                    string fp = filePath;

                    FileStream fs = new FileStream(@fp, FileMode.Create, FileAccess.Write);

                    fs.Write(Received, 0, Received.Length);

                    fs.Close();

                    eventArgs.ReceivedString = filePath;

             //       Log.WriteEntry("Write file com storefiletcom:" + methodName + "  " + paramFrom + paramTo + " " + Received.Length.ToString() + " " + filePath);

                    this.ReceivedData(this, eventArgs);

                }

            }

            catch (Exception ex)

            {

               this.ReturnError ( "Storefilecom Client:" + ex.Message+"From service:"+retError );

            }

            //    ((IClientChannel)Proxy).Close();

            this.Dispose();

        }

       public void StoreFileComAsyn(object source, string methodName, string paramFrom, string paramTo, string filePath)

        {

            ThreadStart starter = delegate { this.StoreFileCom(source, methodName, paramFrom, paramTo, filePath); };

            new Thread(starter).Start();

        }

        private System.Diagnostics.EventLog Log;

        public WcfVfpClient()

        {

            //log file

            if (!System.Diagnostics.EventLog.SourceExists("WcfVfpClient"))

            {

                System.Diagnostics.EventLog.CreateEventSource(

                    "WcfVfpClient", "WcfVfpClient");

            }

            Log = new System.Diagnostics.EventLog();

            Log.Source = "WcfVfpClient";

            Log.Log = "WcfVfpClient";

            Log.WriteEntry("Start from WcfVfpClient");

        }

        public string EndPointUri

        {

            get { return endPointUri; }

            set { endPointUri = value; }

        }

       private WcfVfpWs.IService1Channel _Proxy = null;

       protected WcfVfpWs.IService1Channel Proxy

        {

            get

            {

                if (_Proxy == null)

                {

                    try

                    {

                        EndpointAddress ep = new EndpointAddress(this.endPointUri);

                        if (endPointUri.StartsWith("net.tcp://"))

                        {

                            System.ServiceModel.NetTcpBinding binding = null;

                            binding = new NetTcpBinding();

                            binding.Security.Transport.ProtectionLevel =

                            System.Net.Security.ProtectionLevel.None;

                            binding.Security.Transport.ClientCredentialType =

                            TcpClientCredentialType.None;

                            binding.Security.Mode = SecurityMode.None;

                            binding.TransactionFlow = false;

                            binding.MaxReceivedMessageSize = 600000000;

                            binding.ReaderQuotas.MaxArrayLength = 99999999;

                            binding.TransferMode = TransferMode.Buffered;

                            _Proxy = ChannelFactory<WcfVfpWs.IService1Channel>.CreateChannel(binding, ep);

                        }

                    }

                    catch (Exception ex)

                    {

                        this.ReturnError("Proxy not started" + ex.Message);

                    }

                }

                return _Proxy;

            }

            set { _Proxy = value; }

        }

       public void Dispose()

       {

           try

           {

               if (_Proxy != null)

               {

                   if (_Proxy.State != CommunicationState.Faulted)

                   {

                      _Proxy.Close();

                   }

                   else

                   {

                       _Proxy.Abort();

                   }

               }

           }

           catch (FaultException unknownFault)

           {

               System.Diagnostics.Debug.WriteLine("An unknown exception was received. " + unknownFault.Message);

               _Proxy.Abort();

           }

         catch (CommunicationException)

           {

               System.Diagnostics.Debug.WriteLine("Service proxy communication exception.");

               _Proxy.Abort();

           }

           catch (TimeoutException)

           {

               System.Diagnostics.Debug.WriteLine("Service proxy timeout exception.");

               _Proxy.Abort();

           }

           catch (Exception)

           {

               System.Diagnostics.Debug.WriteLine("Service proxy encountred a unknown exception and aborted");

               _Proxy.Abort();

               throw;

           }

           finally

           {

               _Proxy = null;

           }

       }

        private string _ErrorMessage = "";

        public string ErrorMessage

        {

            get { return _ErrorMessage; }

            set { _ErrorMessage = value; }

        }

        private bool ProxyNotStarted( )

        {

            if (this.Proxy == null)

            {

               this.ReturnError ( "Proxy  not started");

               return false;

            }

            else

            {

                return true;

            }

        }

        private void ReturnError( string retError)

        {

            this.ErrorMessage = retError;

            this.eventArgs.ErrorMessage = this.ErrorMessage;

            Log.WriteEntry("From WcfVfpClient:"+ this.ErrorMessage.ToString() + " "  + retError);

            this.ReceivedData(this, this.eventArgs);

        }

    }

}

- Create a new class ComEventArgs.cs (Listing 7).

Listing 7. The ComEventArgs class

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Data;

namespace ComWfcVfpClient

{

         public class ComEventArgs : EventArgs

        {

           public string ErrorMessage = "";

           public string ReceivedType = "";

           public string ReceivedString = "";

           public object ReceivedByte = null;

           public DataSet ReceivedDataSet = null;

           public DataTable ReceivedDataTable = null;

           public object ReceivedState = null;

           public ComWfcVfpClient.WcfVfpWs.OrderListCollection ReceivedCollection = null;

           public bool  IsDownloadCompleted = false;

           public bool Cancel = false;

           public void receivedData(string receivedType,string receivedString, object receivedByte, ComWfcVfpClient.WcfVfpWs.OrderListCollection receivedCollection, DataSet receivedDataSet,

           DataTable receivedDataTable, object receivedState, bool isDownloadCompleted, bool cancel, string erorMessage)

           {

               this.ReceivedType = receivedType;

               this.ReceivedString = receivedString;

               this.ReceivedByte = receivedByte;

               this.ReceivedDataSet = receivedDataSet;

               this.ReceivedDataTable = receivedDataTable;

               this.ReceivedState = receivedState;

               this. ReceivedCollection=receivedCollection;

               this.IsDownloadCompleted = isDownloadCompleted;

               this.Cancel = cancel;

               this.ErrorMessage = erorMessage;

           }

        }

}

- In the class ComWfcVfpClient.cs (Listing 6) we define the delegate GridEventHandler and its event ReceivedData. We use it to pass the information to the subscribers (collers) of this class instance. We collect the information in the object eventArgs, generate the event and publish it with the command: this.ReceivedData(this, this.eventArgs);

- Add a new Service reference to the WCF service (in our case net.tcp://192.168.123.63:8081/wcfvfpwstcp , see the key uri in app.config in the download). Name it WcfVfpWs. It generate the <client> and <binding> description in the app.config. Double click on the WcfVfpWs service reference in the Solution Explorer and select View in object browser. You see the service description used by the WCF client in conjunction with the app.config.

- In the class ComWfcVfpClient.cs is the method WcfVfpWs.IService1Channel Proxy (Listing 6), which generate the client channel (the proxy) and add it few properties (about security, message size etc). When the client is done, the client needs to close the proxy transport channel [7].

- In the class ComWfcVfpClient.cs we find similar methods as in the WCF service. We call the service methods in two ways: synchronous (blocking) and asynchronous which run the synchronous method on a new thread. For example the method

public void StoreDataSetAsyn(object source, string methodName, string paramFrom, string paramTo)

        {

            ThreadStart starter = delegate { this.StoreDataSet(source, methodName, paramFrom, paramTo); };

            new Thread(starter).Start();

                }

call the synchronous method StoreDataSet on a new thread (non blocking the VFP form). The above code is not a real callback to the server! It is only a »local« callback. The method StoreDataSetAsyn is delegating the task to the method StoreDataSet on a new thread and implicitly also the method (event) »ReceivedData(this, eventArgs)« is fired. It informs (call back) the callers (subscribers) of the results generated by the StoreDataSet. The callers receive the results in the form of two objects (sender – its type is object and eventArgs - of type ComEventArgs). The ComEventArgs type must be known to the caller! (We will present it in next chapters). 

The user control

Create a new project in Visual Studio, select the Interop User Control Library template, and name it NetComGrid.

Listing 8. Net DataGrid control with a COM visible interface

using System.Collections.Generic;

using System.Text;

using System.Configuration;

using System.Threading;

using System.IO;

using System.Runtime.InteropServices;

using System.Windows.Forms;

using System.Data;

using System;

using System.Drawing;

using System.Reflection;

using System.Runtime.Serialization;

using Microsoft.Win32;

namespace NetComGrid

{

    [ComVisible(true)]

    [ClassInterface(ClassInterfaceType.AutoDual)]

    [ComSourceInterfaces(typeof(ComEvents))]

    [ProgId("NetComGrid.GridCom")]

    [Guid(GridCom.ClassId)]

    public partial class GridCom : UserControl

    {

   //guid generated by VS - Tools - Create Guid

        public const string ClassId = "3DDAD93A-3D6F-4084-B7E1-C63FA009B0A7";

        public const string InterfaceId = "37D5B6A3-84A9-4dd4-9C22-FF5CFE886B37";

        public delegate void FormEventHandler(object source, string respStr, bool isComplete, string errMess);

        public event FormEventHandler IsCompleted;

        private string _retOrderId;

        private  ComWfcVfpClient.WcfVfpClient wcfClient = null;

        private int t1;

        private string _method;

        private string _paramFrom;

        private string _paramTo;

        private string _endPointUri;

        private ComWfcVfpClient.ComEventArgs ea;

        public GridCom()

        {

            InitializeComponent();

        }

        public void OnIsCompleted(object source, string respStr, bool isComp, string errMess)

        {

            if (IsCompleted != null)

            {

                IsCompleted(source, respStr, isComp, errMess);

            }

        }

        public DataGridView DataGridC

        {

            get { return this.dataGridView1; }

            set { this.dataGridView1 = value; }

        }

        public string RetOrderId

        {

            get { return _retOrderId; }

            set

            {

                _retOrderId = value;

            }

        }

        public string EndPointUri

        {

            get {

                return _endPointUri; }

            set { _endPointUri = value; }

        }

        public string Method

        {

            get { return _method; }

            set { _method = value; }

        }

        public string ParamFrom

        {

            get { _paramFrom = this.txtOrderIdFrom.Text ;

                return _paramFrom; }

            set { _paramFrom = value; }

        }

        public string ParamTo

        {

            get { return _paramTo; }

            set { _paramTo = value; }

        }

        private ComWfcVfpClient.WcfVfpClient GetClient()

        {

            if (wcfClient == null)

            {

                wcfClient = new ComWfcVfpClient.WcfVfpClient();

                wcfClient.ReceivedData += new ComWfcVfpClient.WcfVfpClient.GridEventHandler(this.CollGrid);

                //adding the endpoint create a new instance of the proxy!!!

                wcfClient.EndPointUri = this.txtEndPointUri.Text;

            }

            return wcfClient;

        }

        public void GridRefresh()

        {

             this.GetClient();

            try

            {

                this.CallData(this, this._method, this._paramFrom, this._paramTo);

            }

            catch (Exception ex)

            {

                 MessageBox.Show(ex.Message);

            }

        }

        private void CallData(object sender, string MethodName, string paramFrom, string paramTo)

        {

            try

            {

                this.lblTime.Text = System.DateTime.Now.ToString();

                wcfClient.StoreResultComAsyn(sender, "ReturnResult", this._paramFrom, paramTo);

           }

            catch (Exception ex)

            {

                MessageBox.Show(ex.Message);

            }

        }

        // the delegate ReceivedData in ComWcfVfpClient pass the execution to this metod (when the data arrived from the WCF) 

        private void CollGrid(object sender, EventArgs e)

        {

            ea = (ComWfcVfpClient.ComEventArgs)e;

            System.DateTime t1 = Convert.ToDateTime(this.lblTime.Text);

            System.TimeSpan dif = System.DateTime.Now.Subtract(t1);

            MethodInvoker uG = delegate

            {

                if (ea.ErrorMessage != string.Empty)

                {

                    this.lblError.ForeColor = System.Drawing.Color.Red;

                    this.lblError.Text = ea.ErrorMessage;

                    MessageBox.Show(ea.ErrorMessage);

                }

                else

                {

                    this.lblError.ForeColor = System.Drawing.Color.Red;

                    this.lblError.Text = "";

                 

                    switch (ea.ReceivedType)

                    {

                        case "D":

                            this.dataGridView1.DataSource   = ea.ReceivedDataSet.Tables[0] ;

                            OnIsCompleted(this, this._retOrderId, true, "");

                            break;

                        case "C":

                            this.dataGridView1.DataSource   = ea.ReceivedCollection;

                            OnIsCompleted(this, this._retOrderId, true, "");

                            break;

                        case "T":

                             this.dataGridView1.DataSource  = ea.ReceivedDataTable;

                             OnIsCompleted(this, this._retOrderId, true, "");

                            break;

                        case "F":

                            OnIsCompleted(ea.ReceivedState, ea.ReceivedString, true, ea.ErrorMessage);

                            break;

                        default:

                            OnIsCompleted(this, ea.ReceivedString, true, ea.ErrorMessage);

                            break;

                    }

                    this.lblTime.Text = (dif.ToString());

                }

            };

            if (this.dataGridView1.InvokeRequired)

            {

                this.dataGridView1.Invoke(uG);

            }

            else

            {

                uG();

            }

        }

        private void btnLocal_Click(object sender, EventArgs e)

        {

            VfpServerO vfpSer = new VfpServerO();

            this.lblTime.Text = System.DateTime.Now.ToString();

            DataSet ds = vfpSer.StoreDataSet("ReturnResult", this.txtOrderIdFrom.Text, "");

            this.dataGridView1.DataSource = ds.Tables[0];

            System.DateTime t1 = Convert.ToDateTime(this.lblTime.Text);

            System.TimeSpan dif = System.DateTime.Now.Subtract(t1);

            this.lblTime.Text = (dif.ToString());

        }

        private void btnService_Click(object sender, EventArgs e)

        {

            this.lblTime.Text = System.DateTime.Now.ToString();

            if (wcfClient == null)

            {

                this.GetClient();

            }

            wcfClient.StoreDataSetAsyn(this, "ReturnResult", this.txtOrderIdFrom.Text, "");

        }

        private void btnCollection_Click(object sender, EventArgs e)

        {

            this._paramFrom = this.txtOrderIdFrom.Text;

            this._method = "ReturnResult";

            this.GridRefresh();

        }

        private void btnTable_Click(object sender, EventArgs e)

        {

            this.lblTime.Text = System.DateTime.Now.ToString();

            if (wcfClient == null)

            {

                this.GetClient();

            }

            wcfClient.StoreDataTableAsyn(this, "ReturnResult", this.txtOrderIdFrom.Text, "");

        }

        public void StoreFileComAsyn(object sender, string method, string OrderIdFrom, string orderIdTo, string filePath)

        {

            this.lblTime.Text = System.DateTime.Now.ToString();

            if (wcfClient == null)

            {

                this.GetClient();

            }

            wcfClient.StoreFileComAsyn(sender, method, OrderIdFrom, orderIdTo, filePath);

        }

        [GuidAttribute(GridCom.InterfaceId)]

        public interface ComEvents

        {

            //the comm interface for the eventhandler IsCompleted

            //On the client (VFP) side we use the command EVENTHANDLER and the

            //pubblic OLE class MyEvents to comunicate with the COM generated from this class

            [DispId(2)]

            void IsCompleted(object source, string respStr, bool icComplete, string errMess);

            [DispId(1)]

            void ReceivedData(object source, ComWfcVfpClient.ComEventArgs e);

        }

        //copyright Morgan Skinner, 2001, create ActiveX control visible in VFP options

        [ComRegisterFunction()]

        public static void RegisterClass(string key)

        {

            // Strip off HKEY_CLASSES_ROOT\ from the passed key as I don't need it

            StringBuilder sb = new StringBuilder(key);

            sb.Replace(@"HKEY_CLASSES_ROOT\", "");

            // Open the CLSID\{guid} key for write access

            RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);

            // And create the 'Control' key - this allows it to show up in

            // the ActiveX control container

            RegistryKey ctrl = k.CreateSubKey("Control");

            ctrl.Close();

            // Next create the CodeBase entry - needed if not string named and GACced.

            RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);

            inprocServer32.SetValue("CodeBase", Assembly.GetExecutingAssembly().CodeBase);

            inprocServer32.Close();

            // Finally close the main key

            k.Close();

        }

        //copyright Morgan Skinner, 2001

        [ComUnregisterFunction()]

        public static void UnregisterClass(string key)

        {

            StringBuilder sb = new StringBuilder(key);

            sb.Replace(@"HKEY_CLASSES_ROOT\", "");

            // Open HKCR\CLSID\{guid} for write access

            RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true);

            // Delete the 'Control' key, but don't throw an exception if it does not exist

            k.DeleteSubKey("Control", false);

            // Next open up InprocServer32

            RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true);

            // And delete the CodeBase key, again not throwing if missing

            k.DeleteSubKey("CodeBase", false);

            // Finally close the main key

            k.Close();

        }

         public void dataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)

        {

            this._retOrderId = dataGridView1.Rows[dataGridView1.CurrentCell.RowIndex].Cells["Orderid"].Value.ToString();

            OnIsCompleted(this, this._retOrderId, true, e.RowIndex.ToString());

    }

        private void dataGridView1_CellDoubleClick(object sender, DataGridViewCellEventArgs e)

        {

            this._retOrderId = dataGridView1.Rows[dataGridView1.CurrentCell.RowIndex].Cells["Orderid"].Value.ToString();

            OnIsCompleted(this, this._retOrderId, true, e.RowIndex.ToString()); //inform the subscribers (in our case the vfp form)

        }

 

    }

   

}

 

Copy the program in Listing 8. Paste it in the class created by the project. Rename the class to ComGrid. (For a detailed description on how to create the Active-X control sees for example [1]. Some notes:

- don't forget to mark the assembly as COM visible (See [1])!

- in order our service could be registered and created we add the frame:

    [ComVisible(true)]

    [ClassInterface(ClassInterfaceType.AutoDual)]

    [ComSourceInterfaces(typeof(ComEvents))]

    [ProgId("NetComGrid.GridCom")]

    [Guid(GridCom.ClassId)]

- the guid created in VS - Tools – Create guid help us to solve many problems with the registration of the class (COM object). We create two guid:

public const string ClassId = "3DDAD93A-3D6F-4084-B7E1-C63FA009B0A7";

public const string InterfaceId = "37D5B6A3-84A9-4dd4-9C22-FF5CFE886B37"

After building the project, using the object browser we find the object under the name NetComGrid.GridCom.

- the interface ComEvents allow us to notify the programs (subscribers) of the events fired in this class (The subscribers receive the messages throw this interface when the events based on the delegate IsCompleted and ReceivedData are fired). 

public interface ComEvents

        {

            [DispId(2)]

            void IsCompleted(object source, string respStr, bool icComplete, string errMess);

            [DispId(1)]

            void ReceivedData(object source, ComWfcVfpClient.ComEventArgs e);

        }

- The two functions framed with [ComRegisterFunction()] register and unregistered the COM class and make it visible as an Active-X control (Visible in VFP – Tools – Options – OleControls). You register the NetComGrid.GridCom control on your development machine when you build the project. On the deployment computer you register the control when you install the project. In the download solution is the project (NetComGrid) and the setup project (NetComGridIn). (How to create and use this project see [4]).

The VFP client

With the VFP form [Figure 1] we retrive the data from the server using five methods:

- Download a file, write it on the disk and bind it to the VFP datagrid (button File).

- Download the data directly from the server using the net OleDb driver (without the WCF infrastructure). - - The received data we bind to the windows datagrid (button Local).

- Retrieve the data as a DatsSet (button DataSet).

- Retrieve the data as a DataTable (button DataTable).

- Retrieve the data as a strong typed Collection (button Collection).

The VFP form has also two other buttons for testing purposes:Test and ComGridRefresh.

Listing 9. The button File click method

LOCAL lrf as NetComGrid.GridCom

paramTo=""

paramFrom=thisform.txtOrdnNo.Value

method="FindDataFile"

EndPointUri=thisform.txtUri.value

lrf=thisform.olecontrol1

LOCAL loEvents as myGridevents OF "myGridevents.prg"

IF VARTYPE(thisform.loEvents)="U"

thisform.AddProperty("loEvents")

thisform.loEvents = NEWOBJECT("myGridevents","myGridevents.prg")

EVENTHANDLER(thisform.olecontrol1.oBJECT ,thisform.loevents) && loEvents is listening the notification of the Com object

BINDEVENT(thisform.loevents,"ComEvents_IsCompleted",thisform,"onreceive" )

endif

IF TRIM(method)==""

lrf.method="ReturnResult"

ELSE

lrf.method=method

endif

IF TRIM(paramFrom)==""

lrf.paramFrom="181100"

ELSE

lrf.paramFrom=paramFrom

ENDIF

IF TRIM(paramTo)==""

lrf.paramTo=""

ELSE

lrf.paramTo=paramTo

endif

IF (LEN(TRIM(lrf.EndPointUri ))=0)

lrf.EndPointUri=EndPointUri

endif

filePath=SYS(2023)+'\'+SYS(2015)+".dbf" && file path to store the received file

SET procedure TO sourceobject.prg ADDITIVE

loSource=createOBJECT("sourceobject")

*newOBJECT("loSource","sourceobject.prg")

loSource.retmethod=METHOD

loSource.bindcontr=.T.

loSource.bindto="thisform.grid1.recordsource"

lrf.StoreFileComAsyn(loSource,method,thisform.txtOrdnNo.value,"",filePath)

 

 

 

Figure1. The VFP form

 

The buttons File click method is in Listing 9. Let us first comments the events chain in this method.With the Eventhandler we connect the events of the grid (thisform.olecontrol1.oBJECT) and program (olepublic class) mygridevents. The command BINDEVENT binds the   mygridevents.ComEvents_IsCompleted method to the forms onreceive method.

Listing 10.The sourceobjec.prg 

DEFINE CLASS sourceobject as custom

name="sourceobject"

retmethod=""

bindcontr=.T.

respstr=""

bindto=""

enddefine

We create the sourceobject  (Listing 10) with four properties and we pass it to the windows grid (oleobject) method StoreFileComAsyn. This object will return from the ComWcfVfpClient. We will catch it in the form onreceive method (Listing 11)(as the source parameter).

 

Listing 11. The form onreceive method

LPARAMETERS  source as object,respStr as string, isComplete as Boolean, errMess as string

*MESSAGEBOX(source.bindto)

IF source.name="sourceobject"

t1=SECONDS()

fname=respStr

bindsource=source.bindto

IF LEN(TRIM(&bindsource))>0

SELECT (&bindsource)

lcdbf=DBF(ALIAS())

USE

&bindsource=""

IF FILE(lcdbf)

ERASE lcdbf

endif

else

SELECT 0

ENDIF

USE (fname)

&bindsource=ALIAS()

thisform.BindControls=.t.

else

thisform.txtOrderId.Value=respStr

RAISEEVENT(this.txtOrderId,"Refresh")

endif

The form onreceive method has the same signature as the mygridevents. ComEvents_IsCompleted method. In its parameter (source) it catches the sourceobject sent by the button file click event. It also accepts the name of the dbf file written to the disk (the parameter respStr). It changes the grid record source property to a new file name received from the WCF service.

The data retrieve cycle

When we request the data from the VFP database using the function storefile we follow this path:

The VFP client method calls the ComGrid function StoreFileAsyn and pass it the parameter storefile (the name of the VFP store procedure) and the object sourceobject.

The ComGrid method wcfClient.StoreFileComAsyn(sender, method, OrderIdFrom, orderIdTo, filePath) fire the event StoreFileComAsyn on the wcfClient object. The command wcfClient.ReceivedData += new ComWfcVfpClient.WcfVfpClient.GridEventHandler(this.CollGrid)  instruct the client to fire the event this.CollGrid on receiving the results from the service. 

Next, we call the method StoreFileCom on a new thread. It will call the method StoreFileCom on the service (server) and will wait for the response from the service. According to the contract, it accepts the error from the server (retError parameter marked as out) and the byte array:

byte[] Received = Proxy.StoreFile(out retError, methodName, paramFrom, paramTo);

It fills the Received buffer with the dbf files bytes and writes the bytes to the disk dbf file. The method passes the file name to the VFP client (see eventArgs.ReceivedString).

We create the object eventArgs = new ComEventArgs() and set its ReceiveString property value to the name of the dbf file. At last we fire the event this.ReceivedData(this, eventArgs). Using the ReceivedData delegate, we pass the data in the eventArgs object to the NetComGrid.GridCom controls method CollGrid. It fires the event:

 OnIsCompleted(ea.ReceivedState, ea.ReceivedString, true, ea.ErrorMessage);

The delegate IsCompleted informs the VFP client about two things:

- the sender (from the VFP client) ea.ReceivedState,

- the received file name and type (F – file).

We bind the OleControl to the myGridevents.prg (see the EVENTHANDLER), so we connect the NetComGrid interface to the methods in this program.

We bind the event ComEvents_IsCompleted to the form method (event) onreceive:

BINDEVENT(thisform.loevents,"ComEvents_IsCompleted",thisform,"onreceive" )

The onreceive method binds the received dbf file to the datagrid.

The mesurement of the WCF performance

Retrieving the data with oledb driver directly from the server computer (VFP database) is very slow, especially on larger data set it can exceed 1 minute (Try the button Local on the VFP form). It is not included in the measurement. We also exclude the two test buttons. We made 10 measurements for the remaining five buttons. We present the results in Table 1 and Table 2.

Table 1. Retrieving time in seconds for the records set of 250 records

 

dbf

Dataset

Collection

DataTable

 

0,53

0,93

0,75

0,75

 

0,17

0,92

0,62

1,56

 

0,64

0,71

1,12

0,51

 

0,57

1,11

0,78

1,01

 

0,17

0,25

0,91

0,42

 

0,64

1,03

0,45

0,79

 

0,93

0,89

0,13

0,98

 

0,28

0,23

0,73

0,56

 

0,87

0,53

0,75

0,54

 

1,04

0,54

0,64

0,34

avverage

0,58

0,71

0,69

0,75

%

100

122

119

129

min

0,17

0,23

0,13

0,34

The average retrieve (and download) time of the records set of 250 records in a dbf file is 0,58  second (Table 1.) , 19% slower is the retrieve time of the same records set using the OrderCollection object, 22% slower is downloading this data in the form of the dataset, and 29% is the download with the data table. There is significant difference of at least 19% if we download the data as a DBF file.

Table 2. Retrieving time in seconds for the records set of 2.000 records

 

dbf

Dataset

Collection

DataTable

 

1,29

2,7

2,29

2,31

 

2,12

1,87

1,79

2,57

 

1,75

2,5

1,57

2,57

 

1,78

2,51

2,37

2,81

 

1,54

2,31

1,95

2,85

 

1,81

2,67

1,81

2,87

 

1,91

2,29

1,85

2,81

 

1,18

2,17

2,35

2,75

 

1,89

1,96

2,41

2,64

 

1,34

2,2

2,28

2,42

average

1,66

2,32

2,07

2,66

%

100

140

125

160

min

1,18

1,87

1,57

2,31

%min/average

71

81

76

87

What is the difference in retrieving the records set of 2.000 records? We download the data with the dbf file in average 1,66 seconds (Table 2), that is 25% faster then downloading a collection, 40% slower is the download of data in the form of the dataset and 66% slower using the data table. We suppose the greater part of the retrieval times is due to serialization and transport through the wire.

The WCF infrastructure and the data retrieve

The WCF service is the infrastructure component, so you rarely change it. Chaging and testing it means a lot of work!. Theoretically, you can change the binding, hosting etc. In practice, this is not a trivial process, so do not do this.

Conclusion

In certain cases, downloading data from the VFP database is time consuming. Using the WCF technology, we can improve the retrieve time for almost two times. The benefit of using it is also that we can do this in parallel with other processes.

Apart this significant time reduction we can optimize the process also inside the WCF infrastructure. The retrieving time of the data with the WCF services depends also from the type of transported data (its serialization etc.). The small samples of experiments we present in this article shows:

-if we download the data and present it in VFP controls, it is faster, if we download the selected data in the form of the dbf file.

-if we present the data in the windows ActiveX control it is less work, if we download the data as the Collection object.

-if the access to the data is public, it is better if we publish the data as the dataset. In this case the retrieve time costs rise for more than 60% compared with the »private« dbf retrieve.

There is place for other optimization of the WCF infrastructure. The minimal retrieval time is 1.18 (DBF) and 1.57 (Collection) (Table 2), that is, theoretically we can reduce the retrieval time for 29% and 24% respectively. 

We work with a DBF of 400 MB and the corresponding index file of 50MB. Changing these sizes, we obtain different results. It depends also from the used hardware, number of concurrent users etc. However, except for very small record set, there is a big time gap between the direct VFP retrieve and using the WCF service. 

References

[1] C.S.Boyd: CoDe Magazine - Article: COM Interop Over Easy

[2] W.Dong:Performance Improvement for WCF Client Proxy Creation in .NET 3.5 and Best Practices

[3] R.Strahl: Hosting a WCF Service in a non-.NET Client

[4] C.Pirish, D.Mulder: Hosting and Consuming WCF Services

[5] M.Nasr: Simple Windows Service Sample

[6] J.Zohil: VFP performance in LAN environment

[7] M.Gacnik: WCF: Security Sessions and Service Throttling 

Downloads

WCFVFPService   WCFClient      NetGrid       VFPform