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

Calling VFP COM components
from .Net and ASP.Net

 

By Rick Strahl

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

 

Last Update:April 24, 2003

 

Code for this article:

http://www.west-wind.com/presentations/VfpDotNetInterop/aspcominterop.zip

 

Other related detailed articles:

Using .NET Components via COM from Visual FoxPro

General VFP Interop

 

 

Note: The code listings in this article use C# for .NET code and Visual FoxPro for COM or client code.

 

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

 

 

Now that .NET is here you've undoubtedly have the urge to use or at least play with the new functionality that the platform provides. Unfortunately migrating to .NET from Visual FoxPro (or most other development languages) is a big step that requires a steep learning curve. Integration between the old and the new will be crucial as a first step to provide for the ramp up time that's needed to get up to speed on the new platform as well as providing vital links between old and new applications. In this article, which is part of a series of .Net Interop articles, Rick looks at how to integrate Visual FoxPro COM components from .Net specifically ASP.Net.

 

For many years the main call from Microsoft has been to build component based applications based on the Component Object Model (COM) by creating components in your language of choice and then exposing those components or classes as COM objects to the operating system.

 

.NET bucks this trend by using a whole new mechanism of interoperability via the Common Language Runtime (CLR) which provides intermediary byte code that is compatible with multiple languages that can output .NET capable byte code that is compiled into executable binaries on the fly by the runtime. What this means is that the days for binary incompatibility are gone. As are some of the issues that have plagued the COM infrastructure – namely the issue of versioning and DLL Hell.

 

However, COM remains very important even with .NET both as a backwards compatibility feature as well as a mechanism to provide interop with the runtime as a whole. For example, many of the scalability features of the Windows operating system are still implemented using the COM+ system. .NET itself also uses COM+ underneath to implement many of the Enterprise features like distributed transaction management, context and process management and many security features. What's exciting here is though that the intricacies of COM+ are mostly hidden behind more user friendly .NET classes that expose the functionality through class interfaces or attribute based descriptors. 

 

However, COM usage in .NET for the developer is played way down and the focus in .NET is on building .NET classes that perform functionality rather than calling out to 'legacy' COM components to perform business logic or other tasks. COM interop is provided more as a backward compatibility feature than a 'moving forward' feature. But because there's a large, large investment in COM by many organizations, COM support in .NET is fairly strong.

 

The good news is that you can access most COM components in .NET without any fuss. It isn't quite as easy as it was say in Visual FoxPro/Visual Basic with late binding and CreateObject(), but with a little bit more work COM interop is easily accomplished. The not so good news though is that there's a performance penalty for using COM objects in .NET resulting in reduced performance when compared to previous COM based technologies. It's clear that the focus of .NET is not necessarily to provide the best hosting environment to COM components but rather to provide a mechanism to allow applications to co-exist until they can be rebuilt with .NET.

 

COM interop in .NET is a two way affair – you can call COM components from .NET and you can call .NET components from classic COM capable applications. Let's look at each of the approaches, how they work and what they offer to the developer.

Calling COM components from .NET

Calling COM components from .Net is likely to be the most commonly used interop scenario as it allows you to integrate existing functionality into new applications that are being built or migrated to the .NET platform.

 

This process uses internal Interop classes in the .NET framework to link and bind to COM components. The standard process to accomplish this is to import the COM component which creates a compiled wrapper .NET class that implements the COM objects interfaces as a .NET class.  Because .NET is a type safe environment this .NET interface is necessary to allow access to the class methods and properties that are essentially running unmanaged code. This wrapper satisfies the compiler for type safety through mapping all the COM properties/parameters/return types to the appropriate .NET types. Behind the scenes the wrapper class makes the appropriate Interop class calls to call the COM component and then maps the parameters and return values to the wrapper class.

 

Those Interop classes can also be accessed directly via code to provide the equivalent of Late Binding, but be advised that this can take a lot more code than you may be used in Visual FoxPro or Visual Basic type languages. You have to use Reflection in .Net to explicitly name the object that you want to make calls on and then specify which method or property and all the parameters to access indirectly. This code can be messy and I wouldn't recommend this approach unless absolutely necessary. However in some cases as we'll see this approach is required in order to work with types that aren't described in a type library. (more on this later).

 

To demonstrate the basics of COM interop let's create a simple object that I've used in the past for ASP COM interop and walk through creating the object in VFP and then making it available in .NET to an ASP.NET page/application. The class is very simple and doesn't do anything fancy – the purpose here is to demonstrate the operation of the interop mechanism not to show what you can do with it.

 

I'll start by creating a COM object in VFP and by showing a few methods at a time. To review the basics of creating a COM object, I'll start with a simple object that serves as a Counter manager that stores and increases named counter values. The first step is to create the object as a COM capable class (listing 1 – asptools.prg).

 

Listing 1 (VFP): A simple Counter VFP COM object

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

DEFINE CLASS ASPTools AS Custom OLEPUBLIC

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

 

*** Custom Properties

cDataPath=LOGFILEPATH

cAppStartPath = ""

oScriptingContext = .NULL.

 

nCounter = 0

 

lError = .f.

cErrorMsg = ""

 

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

* aspTools :: Init

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

***  Function: Set the server's environment. IMPORTANT!

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

FUNCTION INIT

 

*** Make all required environment settings here

*** KEEP IT SIMPLE: Remember your object is created

***                 on EVERY ASP page hit!

SET RESOURCE OFF   && Best to compile into a CONFIG.FPW

SET EXCLUSIVE OFF

SET REPROCESS TO 2 SECONDS

 

SET CPDIALOG OFF

SET DELETED ON

SET EXACT OFF

SET SAFETY OFF

 

*** Add this mainly so that the project will include

*** this stuff here

SET PROCEDURE TO wwUtils ADDITIVE

SET PROCEDURE TO wwAPI ADDITIVE

 

*** IMPORTANT: Figure out your DLL startup path

THIS.cAppStartPath = ADDBS(JUSTPATH(Application.ServerName))

 

*** If you access VFP data you probably will have to

*** use this path plus a relative path to get to it!

*** You can SET PATH here, or else always access data

*** with the explicit path

SET PATH TO (THIS.cAppStartpath)

DO PATH WITH THIS.cAppStartPath + "DATA"

 

ENDFUNC

 

 

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

* aspTools :: IncCounter

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

***  Function: Increments a counter in the registry.

***      Pass: lcCounter  -  Name of counter to increase

***            lnValue    -  (optional) Set the value of the counter

***                          -1 delete the counter.

***    Return: Increased  Counter value  -  -1 on failure

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

FUNCTION IncCounter(lcCounter as String, lnSetValue as Integer) as Integer

LOCAL lnValue

 

lnSetValue=IIF(EMPTY(lnSetValue),0,lnSetValue)

 

oMTS  = CreateObject("MTxAS.AppServer.1")

THIS.oScriptingContext = oMTS.GetObjectContext()

this.oScriptingContext.Item("Response").Write("Inccounter from VFP<p>")

 

IF !USED("WebCounters")

   IF !FILE(THIS.cAppStartPath + "WebCounters.dbf")

        SELE 0

          CREATE table (THIS.cAppStartPath + "WEBCOUNTERS") ;

          (    NAME        C (20),;

               COUNTER       I )

           USE

   ENDIF

   USE (THIS.cAppStartPath + "WEBCOUNTERS") IN 0 ALIAS WebCounters

ENDIF

 

SELE WebCounters

 

LOCATE FOR UPPER(name) = UPPER(lcCounter)

IF !FOUND()

   INSERT INTO WEBCounters  VALUES (lcCounter,1)

   lnValue = 1

ELSE

   IF RLOCK()

      IF lnSetValue > 0

         REPLACE Counter with lnSetValue

      ELSE

         IF lnSetValue < 0

            REPLACE Counter with 0

            DELETE

         ELSE

            REPLACE Counter with Counter + 1

         ENDIF

      ENDIF

      lnValue = Counter

      UNLOCK

   ELSE

      lnValue = 0

   ENDIF  

ENDIF

 

*** For testing

This.ncounter = lnValue

 

RETURN lnValue

ENDFUNC

 

ENDDEFINE

 

Remember that's it's real easy to create a COM object in VFP simply by setting the OLEPUBLIC attribute on the DEFINE CLASS statement of a PRG based class or by setting the OLE Public checkbox on the class property settings of a VCX based class. However, it's equally important to set up your COM object properly to run in the hosted environment, especially if that hosted environment happens to be a service such as Internet Information Server (IIS). The INIT method of the object sets the environment and saves some state information that preserves the startup location of the application and set's the path to it so we can find the data located there.

 

To create this class as a COM object, add this class to a project (I named mine ASPDemos) and COMPILE it into a Multi-Threaded COM Server or use BUILD MTDLL AspDemos from AspDemos from the Command Window.

 

Once this is done you can now use the object from VFP with:

 

oDemo = CREATEOBJECT("AspDemos.AspTools")

? o.IncCounter("CodeDemo")  && 1

? o.IncCounter("CodeDemo")  && 2

? o.IncCounter("EssentialFoxDemo")  && 1

? o.IncCounter("CodeDemo")  && 3

 

For most of you this is nothing new. But note that if you are building COM objects for use with .NET there is at least one major difference: You MUST use VFP 7.0 or later and its typing features to describe your parameter and return value types, or else your methods will not be accessible properly.

Importing the COM object into .NET

Once your object is built you need to import it into .NET. The easiest way to do this is to use Visual Studio.NET and the References selection in the Project manager. Right click on the References tree item and Add Reference. Then pick the COM tab and select your object from the list, or if it's not registered yet, browse to it on disk (Figure 1).

 

Figure 1 – Importing a COM object into .NET is as easy as selecting the object from the installed COM components or physically selecting a typelibrary/dll from disk. This process creates a .NET wrapper class for your COM object that is treated as any other .NET object.

 

Once the object has been added to your project you can access it pretty much like any other .NET object. For ASP.NET pages you'll have to do a couple of additional things to get it to run properly. Since I'm using VS.NET the actual ASP.NET page contains nothing more than the page header, while all the logic goes into a codebehind class which is a file with the .cs extension. The main thing on the ASPX page header is the PAGE directive:

 

<%@ Page ASPCOMPAT=true language="c#" Codebehind="ComInterop.aspx.cs" AutoEventWireup="false" Inherits="ASPInterOp.ComInterop" %>

 

There's another very important directive in the PAGE tag above! The ASPCOMPAT=true attribute tells the ASP.NET page that it is hosting COM components that don't conform to the ASP.NET threading model. Visual FoxPro (and Visual Basic 6) COM objects are Single Threaded Apartment (STA) objects, while .NET natively runs ASP.NET requests on MultiThreaded Appartment (MTA) threads. This requires the .NET framework to make some changes into how requests are handled by essentially switching the request into STA compatible mode. Although you can run without ASPCOMPAT=true and it appears that the COM object is operating correctly, there are subtle problems that occur such as object state not releasing between calls and objects leaking incorrect values from other instances which are caused by VFP components trying to run in the MTA environment. In short, make sure you set ASPCOMPAT=true whenever calling VFP components from ASP.Net.

 

ASPCOMPAT also provides compatibility to classic the ASP COM object environment by providing the appropriate Context objects that were supported to retrieve the ASP intrinsic objects in classic ASP. In particular the ObjectContext object provider which uses the COM+ IApplicationContext object interface is provided to COM objects which would otherwise not be available.

 

The CodeBehind page is where all code is written and it contains all the logic of the page with the ASPX page only containing the display elements. I want to keep things simple so I created a simple form that lets you increment counters, delete them and show them all. The final running form is shown in Figure 2.

 

Figure 2 – The COM Counter sample demonstrates using a basic COM object in a Web form.

 

The actual page is made up of a textbox (txtCounterName), a label (lblCounter), a set of operational buttons (btnIncrement, btnDelete, btnShowAll) and an empty label control that is filled from the COM object – in this case with an HTML string that shows the table you see in Figure 2. There's also a label between the table and the buttons called lblErrorMsg that is used for status information. For example, when you delete a counter this label is updated with a message that says that the counter was deleted. You can view the source code for both the ASPX and cs page in the source archive provided with this article.

 

Let's start with how the basic incrementation mechanism works. Here's the Page Load and Increment button click code (Listing 2 – ComInterop.aspx.cs).

 

Listing 2 (C# ASP.NET): Simple ASP.NET Codebehind class

...

using aspdemos;  // COM object wrapper

 

namespace ASPInterOp

{

   // … control declarations left out

 

   public class ComInterop : System.Web.UI.Page

   {

      private void Page_Load(object sender, System.EventArgs e)

      {

         if (!this.IsPostBack)

         {

            this.btnIncrement_Click(this,e);

         }

      }

 

      private void btnIncrement_Click(object sender, System.EventArgs e)

      {

         ASPToolsClass oVFP = new ASPToolsClass();

         this.lblCounter.Text = 

              oVFP.IncCounter(this.txtCounterName.Text,0).ToString();

      }

}

 

First make sure the aspdemos namespace, which represents the imported COM object, is added to your class so that you can access the class. With the namespace and reference added our VFP class behaves just like any other .NET class including full access to Intellisense

 

It's important that you define your methods with proper Type information using VFP 7.0's parameter typing.

 

FUNCTION IncCounter(lcCounter as String, lnSetValue as Integer) as Integer

 

If you don't provide the type information, the method will not import properly and show up without parameters and return values – and will fail at runtime when called. (Note: as of .NET Framework SP1 this has been fixed – methods with variants will import with object parameters, which still are a pain to use so make sure you use typed parameters/returns on all COM export methods.

 

Properties names are translated to upper case so nCounter becomes NCOUNTER. Remember C# is case-sensitive so this makes a big difference. Better yet, always use Intellisense to provide you with the proper property names when typing code in VS.Net.

 

Note that all properties are returned as type Object unless you specifically type them with VFP's _ComAttrib functionality part of the class definition. Type object is similar but not quite the same as a Variant in COM. Object types can contain different sub types (such as strings, ints etc), but they cannot directly be accessed by their sub-type. In order to use the variable safely in C# you have to explicitly cast it to a specific type or use a conversion function:

 

int lnCounter = (int) oVFP.NCOUNTER + 10;

this.txtCounterProperty.Text = lnCounter.ToString();

 

If you use the object variable multiple times it's probably more efficient to store it into a properly typed variable first, then use it as you intend to. It's both easier to work with and faster in execution.

Changing Code in your COM object

When it comes time to make a change to the COM object, things are very similar to the way it was with classic ASP – in order to compile the COM object you need to restart the Web Service or at least the Web application that hosts the COM component.

 

The easiest way to do this for me is to run the IISRESTART utility. And for this purpose I use an Intellisense script (tied to the IIS key sequence) that simply does:

 

RUN /n4 IISRESET      

 

IISRESET is a IIS 5.0 and later utility that has a number of options for restarting the Web server. It can restart all the services or with the proper options any of the specific services. By the way, it can also be used to reboot a machine reliably. The nice thing about running IISRESET from Fox is that it runs in a separate window that starts and finishes when it's done so there's no more user interaction required.

 

While the reset is working let's add another method to our server to delete a counter (Listing 11 - asptools.prg).

 

Listing3 (VFP): Deleting a counter in the VFP COM code

Function DeleteCounter(lcCounterName as String) as Boolean

 

IF EMPTY(lcCounterName)

   RETURN .F.

ENDIF

 

DELETE FROM WebCounters where Name = lcCountername

 

RETURN .T.

 

Then rebuild your server with BUILD MTDLL AspDemos from ASPDemos. To hook up this method I'll edit the visual ASPX page add the btnDelete button and add the following event code to the click event:

 

Listing 4 (C# ASP.NET): Calling the Delete code from ASP.NET code

private void btnDelete_Click(object sender, System.EventArgs e)

{

   ASPToolsClass oVFP = new ASPToolsClass();

   oVFP.DeleteCounter(this.txtCounterName.Text);

   this.lblErrorMsg.Text = "Counter " + txtCounterName.Text + " deleted...";

   this.lblCounter.Text = "";

}

 

Before you add this code you might want to quickly recompile the project (Ctrl-Shift-B) to force the AspDemos type library code change to reflect in the .NET class and in the Intellisense list. Once you recompile the DeleteCounter() method is available, otherwise it is not. This means that VS.NET rebuilds your COM wrapper every time you make a change to it, which is convenient. But realize that a compile is required to make this happen – changing the class and not recompiling in .NET  will likely result in a runtime error or COM version conflict (depending on if and how the COM interface has changed).

 

This is different from classic ASP – the COM object in .NET is early bound which means it's bound at compile time through the wrapper class. Any changes to the COM object require a recompile, so making a change to the COM object always require two steps:

 

  1. Change and recompile the VFP COM object
  2. Recompile the .NET COM client application

 

 

Types of data to return

I think you will find that if you want to use ASP.NET efficiently, you probably want to use COM objects as business objects as opposed to an output generator. The two examples I've shown so far are - albeit very simple – business object methods that return business data directly back to your application.

 

You may also want to return display output back to the ASP page. For example, you may run a query and based on this query generate HTML in your VFP code. While I don't think this is the best of ideas for ASP.NET Web Form applications, you can do it very easily as the following code demonstrates.

 

Start by creating another method on your COM object that returns in this case the list of counters (Listing 5 – asptools.prg):

 

Listing 5 (VFP): Returning a list of counters in HTML format from the COM object

FUNCTION ShowCounters() as String

 

IF !USED("WebCounters")

   IF !FILE(THIS.cAppStartPath + "WebCounters.dbf")

        SELE 0

          CREATE table (THIS.cAppStartPath + "WEBCOUNTERS") ;

          (    NAME        C (20),;

              VALUE       I )

           USE

   ENDIF

   USE (THIS.cAppStartPath + "WEBCOUNTERS") IN 0 ALIAS WebCounters

ENDIF

 

SELECT name as Counter_Name, value as hits, ;

      [<a href="ShowCounters.asp?Action=Delete&ID=]+name+[">Delete</a>] as Action ;

   FROM WebCounters ;

   order by 1 INTO CURSOR TQuery

 

*** Render the cursor as an HTML table

RETURN THIS.ShowCursor()

 

This method runs a query and then calls a method in the sample called ShowCursor() which generically generates an HTML table from the data. This HTML string is then returned back to the client.

 

In the .NET form add a blank label control and call it lblCounterList. We then assign the control the HTML string that is returned from the ShowCounters() call to the COM object.

 

Listing 6 (C# ASP.NET): Assigning HTML content to a label control

private void btnShowAll_Click(object sender, System.EventArgs e)

{

   ASPToolsClass oVFP = new ASPToolsClass();

   this.lblCounterList.Text =

      "<small><center>Note: To refresh this list click the " +

      "Show All button</center></small>" +

      oVFP.ShowCounters();

}

 

Now, if you run this form you'll notice something interesting if you're not familiar with Web forms. After you click Show All once the list pops up and even if you click one of the other buttons, the list stays on the form. But also notice that the value of the counters don't change in the list even though you may have clicked Increment a few times and the actual counter values has been upped.

 

What's happening here is something called Viewstate. Viewstate holds the content of server variables – including labels – in an encoded, hidden form variable that is posted back to the server. So the HTML string is compressed and sent back to the server decoded there and then redisplayed. What this means is that the list that you see if you don't click on the Show All button is a static string rather than a live result retrieved from the COM object. It gets re-posted from its previous form state. As such it gets to be out of sync in this scenario.

 

There are two ways to solve this issue. You can either check to see if the list is active and if so always reload it from the COM object. Or, probably more efficiently, turn off the Viewstate for the label control and show the list only when the user explicitly asks to see it by clicking on the Show All button which is the approach I took here.

 

This is a good example of how Viewstate affects your ASP.NET Web forms and data displayed in it – you will find you may have to write a fair amount of code to properly handle data especially if you're dealing with data intensive controls/fields on a form and deciding on whether to support Viewstate on these controls for ease of use or ending up sending a ton of data over the wire to persist the state. A trade off you have to work out for your specific scenario.

Returning non COM objects

One important area is that of returning objects back through COM interop and .NET. Specifically if you want to return objects that aren't defined in the type library of the COM object you are publishing. I do this quite frequently with code shown in Figure 7 (asptools.prg).

 

Listing 7 (VFP): Returning a record object over COM

FUNCTION GetCustObject(lcCustId as String) as Object

 

IF !USED("TT_Cust")              

   USE (THIS.cAppStartPath +"wwdemo\TT_Cust") IN 0

ENDIF

 

SELE TT_Cust

 

LOCATE FOR CustNo=PADL(lcCustId,8)

IF FOUND()

   SCATTER NAME loCustomer MEMO

   RETURN loCustomer

ENDIF  

 

SCATTER NAME loCustomer MEMO BLANK

 

RETURN loCustomer

 

In classic ASP or a COM capable client you can simply do this:

 

Set oVFP = Server.CreateObject("aspdemos.asptools")

Set loCustomer = oVFP.GetCustObject("4")

Response.Write(loCustomer.COMPANY)

 

But this doesn't work in ASP.NET because the SCATTER NAME object is not defined in the Type library that was imported by VS.NET to create the class wrapper. .NET has no idea how to reference the returned object directly.

 

So how do we do this? The pointer returned is an IDispatch object pointer and we have to access this object indirectly now because .NET can't do the automatic mapping as VBScipt/Jscript ASP was able to do. Unfortunately, this is a bit more work in .NET (Listing 8 – ComInterop.aspx.cs).

 

Listing 8 (C#): Retrieving a property dynamically

private void btnObject_Click(object sender, System.EventArgs e)

{

   ASPToolsClass oVFP = new ASPToolsClass();

   object loCustomer = oVFP.GetCustObject("4");

  

   // string lcCompany = loCustomer.COMPANY;  // no good!

 

   object temp =  loCustomer.GetType().InvokeMember(

                  "COMPANY",BindingFlags.GetProperty,null,loCustomer,null);

   string lcCompany = (string) temp;

   this.lblErrorMsg.Text = lcCompany;

}

 

Messy, huh? Unfortunately this is the only way to retrieve values that aren't explicitly available in the type library. As you might expect this mechanism is fairly slow as well, as .NET must look up the type info on the object before retrieving the data. To do so it uses Reflection, which is .NET's extensive type discovery and parsing mechanism. In order to use the above code make sure you add the

 

using System.Reflection;

 

namespace to your source code.

 

You can simplify the above code a little by using a wrapper method like this (Listing 10 – ComInterop.aspx.cs).

 

Listing 10 (C#): A simpler way to call a dynamic property with a wrapper

private object GetProperty(object loObject,string lcProperty)

{

   return loObject.GetType().InvokeMember(

                lcProperty,BindingFlags.GetProperty,null,loObject,null);

}

 

which reduces the application code to:

 

this.lblErrorMsg.Text = (string) this.GetProperty(loCustomer,"COMPANY");

 

which is considerably more user friendly.

 

Calling methods dynamically works the same way. The AspTools class includes a method called ReturnObject which can return any object available in the project, whether it's defined as a COM object or not. Let's say you return a full VFP object that isn't a COM object. Listing 11 shows a simulated business object class that simply loads a customer object into an oData member using a Load method (Listing 11 – asptools.prg).

 

Listing 11 (VFP): Business object that with customer load method

DEFINE CLASS aspCustomer as Session

 

oData = null

cAppStartPath = ""

 

FUNCTION INIT

*** We need a startup path to find our data

THIS.cAppStartPath = ADDBS(JUSTPATH(Application.ServerName))

SET PATH TO (THIS.cAppStartpath)

DO PATH WITH THIS.cAppStartPath

 

ENDFUNC

 

*** Load a customer by ID into the oData member

FUNCTION Load(lcCustId as String) as Boolean

 

IF !USED("TT_Cust")        

   USE (THIS.cAppStartPath + DATAPATH + "TT_Cust") IN 0

ENDIF

 

SELE TT_Cust

 

LOCATE FOR CustNo=PADL(lcCustId,8)

IF FOUND()

   SCATTER NAME THIS.oData MEMO

   RETURN .T.

ENDIF  

 

SCATTER NAME THIS.oData MEMO BLANK

 

RETURN .F.

ENDFUNC

* aspCustomer :: Load

 

ENDDEFINE

 

We can now use our COM object to return a reference to this class with the ReturnObject method of the ASPTools class:

 

Listing 12 (VFP): Returning any object from within the project over COM

FUNCTION ReturnObject(lcName as String) as Object

oObject = CREATEOBJECT(lcName)

IF VARTYPE(oObject) # "O"

   RETURN .NULL.

ENDIF

RETURN oObject

 

If you now compile the project you can do the following from the VFP command window:

 

oDear = CREATEOBJECT("aspdemos.asptools")

oCust = oDear.ReturnObject("aspCustomer")

? oCust.Load("4")

? oCust.oData.Company

? oCust.oData.Careof

 

We're dynamically creating a VFP object returning it over COM and then call a method on this object, which in turn sets the oData member with the actual values for the retrieved customer.

 

With .NET we have to use the Reflection interface to perform all these tasks. Let's start by looking at how to make the method call:

 

Listing 13 (C#): Calling a dynamic method in a returned COM object.

ASPToolsClass oVFP = new ASPToolsClass();

object loCustomer = oVFP.ReturnObject("aspCustomer");

 

string lcCompany =  loCustomer.GetType().InvokeMember("Load",

   BindingFlags.InvokeMethod,null,loCustomer,new object[] {"4"}).ToString();

 

this.lblErrorMsg.Text = lcCompany;

 

The method call returns a value or true or false which is stored into the label. As with the property retrieval the Reflection functionality is used to dynamically access the COM object and call the method. This time we need to specify a parameter list in an object array which is the last parameter shown here. The single parameter here is a string of "4". As with the property retrieval this code is messy and hard to remember so I created a wrapper method called CallMethod:

 

Listing 14 (C#): Wrapper method to call a dynamic method on an object

private object CallMethod(object loObject,string lcMethod, params object[] loParams)

{

    return loObject.GetType().InvokeMember(lcMethod,

                BindingFlags.InvokeMethod,null,loObject,loParams);

}

 

This code relies on C#'s ability to pass variable parameters to a method using a parameter array loParams. This creates an array of objects (type object[]) that you can simply pass to InvokeMember() for the last parameter.

 

Using those two helper methods lets look at how we can handle the Fox code from above in .NET (Listing 15– ComInterop.aspx.cs).

 

Listing 15: Accessing a business object dynamically

private void btnObject2_Click(object sender, System.EventArgs e)

{

   ASPToolsClass oVFP = new ASPToolsClass();

   object loCustomer = oVFP.ReturnObject("aspCustomer");

 

   string lcCompany = "";

   if ( (bool) this.CallMethod(loCustomer,"Load","4") )

   {

      object loData = this.GetProperty(loCustomer,"ODATA");

      lcCompany = (string) this.GetProperty(loData,"COMPANY");

   }

 

   this.lblErrorMsg.Text = lcCompany;

}

 

 

Ah, much better. Less code and much more readable and you might actually remember this syntax. As you can see using this sort of logic you can drill down into any kind of object that returns data dynamically but you will have to know its exact interface – there won't be any Intellisense to help you out. This applies not just to FoxPro objects returned as non-COM objects, but also to many system type objects that Windows publishes such as ADSI (Active Directory) and WMI (Windows Management Instrumentation) which publish their provider specific interfaces dynamically without support in type libraries. As long as you know what the names of properties and methods are you can retrieve the data easily.

 

You can further simplify this with the following method which allows you to use a single call to drill down multiple property hierarchy levels:

 

Listing 16: Accessing dynamic properties hierarchically

public object GetPropertyEx(object loParent, string lcProperty)

{

    int lnAt = lcProperty.IndexOf(".");

    if ( lnAt < 0)

          return  loParent.GetType().InvokeMember(lcProperty,

                System.Reflection.BindingFlags.GetProperty,

                null,loParent,null);

 

    // *** Walk the . syntax

    string lcMain = lcProperty.Substring(0,lnAt);

    string lcSubs = lcProperty.Substring(lnAt+1);

 

    object loSub = loParent.GetType().InvokeMember(lcMain,

          System.Reflection.BindingFlags.GetProperty,

          null,loParent,null);

 

    // *** Recurse until we get the lowest ref

    return GetPropertyEx(loSub,lcSubs);

}

 

This code uses recursion to walk through the 'dot' syntax of the object and drill into the lower level objects. With this method you can now do this:

 

string lcCompany = "";

if ( (bool) this.CallMethod(loCustomer,"Load","4") )

   // *** Access loCustomer.oData.Company

   lcCompany = (string) this.GetPropertyEx(loCustomer,"oData.Company");

 

This makes it possible write no more lines of code than you'd write with direct access, even if the syntax is a little more work here.

 

Note that there a number of other invocation modes available including the ability to read 'fields' (as opposed to properties) and set property/field values both on .NET and COM objects dynamically. All this is possible through the Reflection interface that .NET publishes and this provides powerful runtime support for type discovery.

 

Late Binding in .Net

You can also use late binding from within .Net to access a COM component. Here's an example, calling a VFP runtime exe file Help Builder.

 

Listing 16.2 (C#): Calling a VFP COM component with late binding

using System.Runtime.InteropServices;

 

public static void OpenHelpBuilder(ref object loHelp,string lcHelpId)

{

    if (loHelp == null)

    {

        Type loT = Type.GetTypeFromProgID("wwHelp.wwHelp");

        loHelp =  Activator.CreateInstance(loT);

    }

 

    if (lcHelpId != null && lcHelpId.Length > 0)

        ComUtils.CallMethod(loHelp,"OpenForm",lcHelpId,0);

}

 

The key methods are GetTypeFromProgId() which gives a Type reference to .Net and Activator.CreateInstance() which is used to create an actual generic object reference to .Net. You get no Intellisense on this object reference and you have to access this object indirectly through the various method and property access methods found in ComUtils.cs.

 

Calling COM objects in this fashion is more work, but unlike other COM environments, there's really no extra penalty for this latebinding code. As a matter of fact the code that .Net generates for the wrapper object flows through these same code logic. Using late binding also has the advantage of being version independent so if a change to the COM object occurs the .Net client code will continue to work even if the Interface ClassIds might have changed.

Passing ASP.NET objects to VFP COM objects

If you've used COM with classic ASP you might recall that ASP used to publish a couple of interfaces that made it possible to access the ASP intrinsic objects such as Request, Response, Server, Application and Session etc. directly from within VFP code. With ASP.NET this functionality is still supported through the ASPCOMPAT directive on an ASP page.

 

The IScriptingContext interface no longer works. IScriptingContext in classic ASP caused two methods – OnStartPage and OnEndPage – to fire when an ASP request occurred. OnStartPage automatically passed Context object as a parameter. The Context object provides the ability to retrieve the various intrinsic objects. This interface no longer appears to work, even though the ASP.NET docs state that it should work (no word on whether this is a bug in .NET or VFP).

 

More recently Microsoft recommended using the ObjectContext interface instead of IScriptingContext, and this interface properly works with VFP COM objects. The ObjectContext class provides the same functionality as the IScriptingContext Context container object in addition to provide COM+ services and transaction management. The ObjectContext object is based and uses the COM+ services to provide the functionality to VFP.

 

To access the object in VFP and retrieve ASP object you can use the following method of the AspTools class example as a reference (Listing 17 – asptools.prg).

 

Listing 17 (VFP): Accessing ASP.NET/ASP intrinsic object from VFP COM code

FUNCTION ASPObjects() as Void

 

*** Grab the object context

oMTS  = CreateObject("MTxAS.AppServer.1")

oContext = oMTS.GetObjectContext()

 

*** Retrieve some of the ASP objects

loRequest = oContext.item("Request")

loResponse = oContext.item("Response")

loSession = oContext.item("Session")

 

lcOutput=""

THIS.cErrorMsg=""

 

*** Query String

lcOutput = "<PRE>QueryString (Method): " + loRequest.QueryString("Method").item()+"<br>"

 

*** Full QueryString

lcOutput = lcOutput + "Full QueryString: " + loRequest.QueryString("").item() + "<BR>"

 

*** Server Variable

lcOutput = lcOutput + "Browser: " + loRequest.ServerVariables("HTTP_USER_AGENT").item() + "<BR>"

 

*** Form Variable

lcOutput = lcOutput + "Form Company: " + loRequest.Form("txtCompany").item() + "<BR>"

 

*** Accessing and assigning Session Vars

lcOutput = lcOutput + "Session VFPCreatedSession: "

IF !ISNULL(loSession.Value("VFPCreatedSession") )

   *** Show it if it exists

   lcOutput = lcOutput +  loSession.Value("VFPCreatedSession") + "<p>"

ELSE

   *** Otherwise set the session var

   loSession.Value("VFPCreatedSession") = "VFPCreatedSession Value " + TIME()

ENDIF

 

lcOutput = lcOutput + "<h2>Query String</h2>"

loQueryString = loRequest.QueryString

FOR EACH lcFormVar in loQueryString

   lcOutput = lcOutput + lcFormVar + "=" + ;

              loRequest.QueryString(lcFormVar).Item() + CR

ENDFOR  

 

lcOutput = lcOutput + "<h2>Server variables</h2><PRE>" + CR

loServerVars = loRequest.ServerVariables

FOR EACH lcFormVar in loServerVars

   IF lcFormVar="ALL_HTTP"

      LOOP

   ENDIF

   lcOutput = lcOutput + lcFormVar + "=" + ;

              loRequest.ServerVariables(lcFormVar).Item() + CR

ENDFOR   

 

*** Form Vars

lcOutput = lcOutput + "<h2>Form Variables</h2>" + CR

loFormVars = loRequest.Form

FOR EACH lcFormVar in loFormVars

   lcOutput = lcOutput + lcFormVar + "=" + ;

              loRequest.Form(lcFormVar).Item() + CR

ENDFOR  

 

lcOutput = lcOutput + "<PRE>"

 

loResponse.Write(lcOutput + THIS.cErrorMSg)

RETURN

 

To call this from ASP.NET simply use the code in Listing 18 (AspObjects.aspx).

 

Listing 18 (C# ASP.NET): Sample form to demonstrate ASP objects in VFP

<%@ Page ASPCOMPAT=true language="c#" Codebehind="AspObjects.aspx.cs" AutoEventWireup="false" Inherits="ASPInterOp.AspObjects" %>

<html>

    <h2>VFP access to ASP intinsic objects</h2>

    <hr>

    <form action="AspObjects.aspx?Method=Test&Parm2=Another+Parm" METHOD="POST">

          LastName: <input name="txtLastName" value='<%= Request.Form["txtLastName"]%>'><br>

          Company: <input  name="txtCompany" value='<%= Request.Form["txtCompany"]%>'><p>

                <input type="Submit" name="btnSubmit">

    </form>

 

<%

if (Request.Form["btnSubmit"] != null )

{

  Response.Write("<hr><i>Generated from within Visual FoxPro:</i><p>");

  ASPToolsClass oVFP = new ASPToolsClass();

  oVFP.ASPObjects();

}

%>

 

</html>

 

The code in the page checks to see to only access the Fox server if the form was submitted, then calls the method and echos back the request information in HTML format as generated by the Fox server.

 

For those of you familiar with classic ASP and COM note that there are a few subtle differences in behavior of the various objects provided. For example, you can no longer access the various collections without parameters. For example, Request.QueryString() used to return the full querystring in ASP, but fails in ASP.NET. Basically any access of a collection item that doesn't exist returns a NULL rather than a null string (""), which means you may need additional error handling if you can't guarantee that a value is actually available. Otherwise things like this:

 

Request.Form("txtUnPostedVar").Item()

 

Will fail with a data type error as the Item method doesn't exist on a null reference.

 

Furthermore be aware that if you use the Response object in combination with ASP.NET Web forms that the results may not be exactly what you expect. Web Forms do not use the standard Response stream and place text only after the entire page has rendered. So if you use Response.Write before the page has rendered the output from it will likely end up at the very top of the page before the <html> tag of the ASP.NET page. Using Response.Write from within VFP thus only makes sense if the COM object will generate the entire HTTP output, or if the calling ASP.NET page also uses Response output rather than the Web Forms engine (ie. no Server Controls).

Passing DataSets as XML between VFP and .NET

As described earlier.NET uses a new data access metaphor in the ADO.NET architecture by utilizing DataSets and XML to pass data around applications. For VFP applications this means that the easiest way to pass data to .NET often is in the form of XML. VFP's CURSORTOXML() is capable of exporting data in a format that .NET understands and can import into a DataSet fairly easily. Consider the following method returning a cursor in XML format to the ASP.NET page(Listing 19 – asptools.prg).

 

Listing 19: Returning XML from a VFP query

FUNCTION XMLCustList(lcCompany as String) as String

 

*** VFP Sample Data

lcDataPath = VFPSAMPLEDATA

this.cDataPath = lcDataPath

 

lcWhere = ""

IF !EMPTY(lcCompany)

   lcWhere = " WHERE Company = lcCompany "

ENDIF

 

SELECT company,contact,PADR(TRIM(city) + ", " + Country,60) as Location,Phone ;

   FROM (lcDataPath + "Customer") ;

   &lcWhere ;

   ORDER BY company ;

   INTO CURSOR TQuery

 

lcXML = ""

 

CURSORTOXML(ALIAS(),"lcXML",1,32,0,"1")

 

RETURN lcXML

 

To use this data in .NET in a DataSet you can load a new DataSet object from the returned XML using code like the following (Listing 20 – ComInterop.aspx.cs).

 

Listing 20: Loading a DataSet from XML returned by VFP

private void btnDataSet_Click(object sender, System.EventArgs e)

{

   ASPToolsClass oVFP = new ASPToolsClass();

 

   string lcXML = oVFP.XMLCustList("");

 

   DataSet loDS = new DataSet();

   loDS.ReadXml( new StringReader(lcXML) );

     

   this.oGrid.DataSource = loDS.Tables[0];

   this.oGrid.DataBind();        

}

 

The ReadXml method of the DataSet can read XML returned from VFP and import into a DataTable with all of the type information intact. For example, if you returned the MaxOrdAmt field you can check the type with:

 

this.lblErrorMsg.Text = loDS.Tables[0].Rows[0]["MaxOrdAmt"].GetType().Name;

 

which returns 'decimal' which is .NET's translation for the currency type in VFP.

 

You can also pass XML as a string back to the VFP application. Unfortunately VFP 7.0 has no built in way to utilize this XML directly using say XMLTOCURSOR(). There are a couple of problems with turning DataSets into VFP data – the schema information of the datasets do not necessarily contain all the data needed to create a VFP cursor from the data, so a parser would have to make two passes through the data to create the field sizes. This can create problems with field sizing and has potential performance problems.

 

One way that you can import DataSets in VFP from XML is to use wwXML with some manual intervention. Consider the following ASP.NET page that creates a DataSet from the SQL Server Pubs database then passes the DataSet to VFP via an XML string (Listing 21 – ComInterop.aspx.cs).

 

Listing 21 (C# ASP.NET): Passing DataSet XML to a VFP COM method

private void btnDataSetToXML_Click(object sender, System.EventArgs e)

{

   string lcID = "%";

 

   DataSet ds = new DataSet();

 

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

   SqlConnection   oConn = new SqlConnection(cConnection);

  

   SqlDataAdapter oAdapter = new SqlDataAdapter();

   oAdapter.SelectCommand =

      new SqlCommand("select * from Authors where au_id like '" +

                     lcID + "%'",oConn);

        

   oConn.Open();

 

   oAdapter.Fill(ds,"authors");

 

   oConn.Close();

 

 

   // *** This code passes the DataSet to VFP as XML

   ASPToolsClass oVFP = new ASPToolsClass();

 

   StringWriter loWriter = new StringWriter();

   ds.WriteXml( loWriter );

 

   // *** VFP returns an HTML table of the data passed to the method

   this.lblCounterList.Text = oVFP.XMLDataSet(loWriter.ToString(),"authors");

}

 

The code in the VFP COM object uses the XML to create a cursor, and then just to demonstrate that it got the data returns it as an HTML table. You can use wwXML (included with the samples) which has a method called DataSetXMLToCursor to create a VFP cursor from an individual DataTable contained within a DataSet. Because of the limitations of the schema published by a DataSet, wwXML requires that you pre-create the cursor that the data is imported to (Listing 22 – asptools.prg).

 

Listing 22 (VFP): Importing a DataSet from XML using the wwXML class.

FUNCTION XMLDataSet(lcXML as String,lcCursor as String) as String

 

oXML = CREATEOBJECT("wwXML")

CREATE CURSOR Temp (au_id c(10), au_lname c(15), Contract L, PK I)

oXML.DataSetXMLToCursor(lcXML,"temp","authors")

 

*** Just echo the data back as an HTML string

RETURN this.ShowCursor()

 

Note the CREATE CURSOR command to pre-create the table that you're importing to. wwXML uses the structure of the table to import the XML data into the cursor thus avoiding some of the schema limitations. But this limitation also means that you have to know what type of data you're expecting to import beforehand.

 

VFP 8.0 offers another solution by using the XMLAdapter class to import data from a dataset (Listing 23 asptools.prg):

 

Listing 23 (VFP): Importing a DataSet with the XMLAdapter class in VFP 8

FUNCTION XMLDataSet(lcXML as String,lcCursor as String) as String

 

*** Must pre-create cursor if we want to get exact structure

*** from non Fox datasources

CREATE CURSOR Authors (au_id c(10), au_lname c(15), Contract L, PK I)

 

oXA = CREATEOBJECT("XMLAdapter")

oXA.LoadXml(lcXML)

 

*STRTOFILE(lcXML,"d:\temp\AuthorDataSet.xml")

 

oXA.Tables.Item(1).ToCursor(.T.,"Authors")

 

RETURN this.ShowCursor()

ENDFUNC

*  aspTools :: XMLDataSet

 

This example still pre-creates the cursor and uses the .T. parameter for the ToCursor() method to force the import to import into the existing cursor. The XML adapter will not properly import a schema that comes from a non-VFP datasource. The reason for this is that SQL Server for example does not provide all the information a VFP cursor requires such as the size for strings and numeric fields and so XMLAdapter has to guess at the field lengths which results in best guess scenario. In the current beta version of VFP 8 this means that character fields come in as memos and some numeric fields may end up as floats instead of ints or generic numbers. Ultimately to match an exact cursor type you'll have to pre-create your cursor.

 

Using the XML Adapter also makes it possible to modify the data and send it back to the .Net application using an XML Updategram. However, this doesn't work properly in the current beta version I've been using so I couldn't test this further.

Debugging your COM server

Debugging in .Net is just as complicated as it is in ASP classic as the COM object is a compiled application that runs inside of IIS, so by default you can't debug the COM server. However, with a little trickery you can actually debug your components by using the VFP IDE to instantiate the object for you and return it to the ASP.Net page. This idea was originally from Maurice de Beijer who posted an interesting article on how to pass control to VFP via Accessibility objects ( http://www.theproblemsolver.nl/aspdevelopmentanddebugging/index.htm).

 

I had problems with this approach and found a slightly different, but similar way to accomplish the same thing by adding a property and a couple of methods to my COM server:

 

Listing 24 (VFP): Returning a debuggable instance of a VFP COM Object

oVFPDebug = .NULL.

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

* ASPTools :: GetDebugInstance

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

***  Function: Allows debugging of the server

***            Pops up debugger on SET STEP or dialog on error

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

FUNCTION GetDebugInstance as Object

LOCAL loObject

 

THIS.oVFPDebug = CREATEOBJECT("VisualFoxPro.Application.7")

THIS.oVFPDebug.Visible = .T.

THIS.oVFPDebug.DoCmd("cd " + THIS.cAppStartPath)

 

*** Loader for classlibs (header of this file)

*** Makes sure that the class is found and can load

THIS.oVFPDebug.DoCmd("DO ASPTools") 

 

loObject = THIS.oVFPDebug.Eval("CREATE('asptools')")

RETURN loObject

ENDFUNC

*  ASPTools :: GetDebugInstance

 

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

* ASPTools :: CloseDebugInstance

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

***  Function: Shuts down the debug instance created with

***            GetDebugInstance

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

FUNCTION CloseDebugInstance()

 

THIS.oVFPDebug.visible = .F.

THIS.oVFPDebug = 0

 

ENDFUNC

*  ASPTools :: CloseDebugInstance

 

This approach basically instantiates an instance of the Visual FoxPro IDE as a COM object and creates an instance of the object and returns that instance back to the ASP.Net page.

 

To access this functionality from .Net you'd use code like this:

 

Listing 25 (C#): Using a debuggable instance of the VFP COM component

ASPToolsClass oVFP = new ASPToolsClass();

object loVFP = oVFP.GetDebugInstance();

 

// *** casting doesn't work as we have an IDispatch pointer

// ASPToolsClass loTemp = (ASPToolsClass) loTemp;

 

// *** We'll have to use indirect calls with Reflection

string lcHtml = (string) ComUtils.CallMethod(loVFP,"HelloWorld");

int lnId =(int) ComUtils.CallMethod(loVFP,"GetProcessId");

 

oVFP.CloseDebugInstance();

 

Voila – you can debug your VFP COM server in real time. Set a breakpoint in any of the methods called and the debugger will pop up. Note that you will not be able to directly access the new reference and have to use the indirect method calling mechanisms described earlier like CallMethod() and GetProperty() if you are using C# or VB.Net with Option Strict. The reason for this is that the ASPToolsClass is a wrapper for the actual COM object, but not the COM object itself. This means the COM object can't be cast to an ASPToolsClass object and you need to use indirect method and property access.

 

However, with this in place you can now set breakpoints in your code and the debugger will pop up for you as needed. Any failures too will pop up an error dialog in the VFP IDE and you can examine the state of the application in the debugger. While not optimal because of the indirect object reference requirement for you object, this should nevertheless be a big help in debugging COM components.

 

This works in classic ASP as well, and is in fact a little easier.

 

<%

SET oVFP = Server.CreateObject("aspdemos.asptools")

 

set loVFP = oVFP.GetDebugInstance

Response.Write( loVFP.IncCounter("EssentialFox",0) )

 

oVFP.CloseDebugInstance()

%>

 

Here you can call methods directly and you can simply rename your main server reference to something else and assign the debug object to the object var instead which means you can debug by simply adding the GetDebugInstance() and CloseDebugInstance() calls.

 

Required Configuration

In order for this approach to work you will have to configure Visual FoxPro in DCOMCNFG (or Component Services DCOM). To do so:

 

  1. Run DCOMCNFG from the Run box
  2. Select Visual FoxPro Application 8.0 (or 7.0)
  3. Go to the Identity tab and set it to The Interactive User
  4. Go to the Security tab and create custom Launch and Access Permissions
  5. Add the ASPNET account (or whatever your application is running under)
  6. Optionally add IUSR_<machinename> if you also want to debug classic ASP

 

Step 3 makes sure that VFP will be able to become visible on the desktop when called from the ASP.Net service. Without this step VFP would start but hang on any breakpoints because it wouldn’t be visible.

 

ASP.NET and VFP COM object performance

ASP.NET changes the mechanism of how COM objects are accessed considerably compared to classic ASP. As a result COM interop from ASP.NET tends to be somewhat slower than it was with classic ASP.

 

The main difference is that ASP.NET uses the Multi-Threaded Apartment (MTA) model to run internal components, while classic ASP uses the Single Threaded Apartment (STA) model. Visual FoxPro (and Visual Basic) COM objects are STA components – each object instance runs on a single thread and must always be accessed on the same thread that created it. MTA components can run on any thread and be invoked from different threads, because they are assumed to be thread safe. The MTA provides for potentially higher performance as the system has to do much less work to fire a method of an object as it doesn't have to keep track of which thread created the component. The MTA model is a big reason for the improved performance of ASP.NET as a Web backend engine in combination with the compiled nature of the code that it runs.


Natively ASP.NET runs in the MTA model and in order to run VFP/VB components the ASPCOMPAT page directive is required to switch the page in STA model operation. This has several implications. First pages that use ASPCOMPAT must be managed differently than pages that are running in MTA – the system once again must track threads and make sure components run on the proper threads that they were created on. This switch affects performance of the individual page as well as the overall ASP system while any pages that require ASPCOMPAT are active.

 

In addition, ASP.NET is optimized for managed code and calling out into unmanaged COM components requires some overhead.

 

All of this is to say that running COM components in ASP.NET is going to be slower than running them in classic ASP applications. I ran a few tests on the IncCounter example I previously mentioned. To keep things simple I only used Response.Write() output code when I tested the performance with roughly identical ASP and ASPX pages. I tried the component both with and without registering it under COM+. To test I ran Visual Studio Test Center, created a test script with just a single page in it (the ASP or ASPX page). I then ran through 5 sets of 5 minute tests against a single URL and took the average value from these tests (which were very consistent BTW). The following Test Machine was used: Pentium III 1ghz, 512 megs memory Windows 2000 Advanced Server SP2). I also made sure that the ASP.NET application was re-added to the .NET project and recompiled each time the component was moved in and out of COM+. The ASP .NET application also was set to Release mode and the configuration settings with debug settings turned off – IOW, optimized for production testing (incidentally not doing so  as I did when I ran the first few tests didn't affect performance all that much – less than 2% on these tests).

 

I kept the tests really simple to measure mainly the COM overhead. The classic ASP code is just:

<%

SET oVFP = Server.CreateObject("aspdemos.asptools")

Response.Write( oVFP.IncCounter("EssentialFox",0) )

%>

 

The ASP.NET code is:

 

private void Page_Load(object sender, System.EventArgs e)

{

   ASPToolsClass oVFP = new ASPToolsClass();

   Response.Write( oVFP.IncCounter("EssentialFox",0) );

}

 

Here are the results:

 

 

Classic ASP

ASP.NET

Plain COM

118 rps

99 rps

COM+ Component

61 rps

67 rps

 

Notice that classic ASP is about 20% faster for plain operation. It's interesting that the COM+ component access is considerably slower both for classic ASP and ASP.NET so much so that the overhead probably removes the actual performance differences between Classic and ASP.NET. In fact with COM+ components .NET is actually slightly faster.

 

The ASP code here is very simple – it simply instantiates the object and does a Response.Write() on the result from IncCounter(). This demonstrates basically the call overhead of the methods plus a basic data access operation in the VFP code. The numbers change drastically if you now add a few calls to these methods

 

<%

SET oVFP = Server.CreateObject("aspdemos.asptools")

Response.Write( oVFP.IncCounter("EssentialFox",0) )

Response.Write( oVFP.IncCounter("EssentialFox1",0) )

Response.Write( oVFP.IncCounter("EssentialFox2",0) )

Response.Write( oVFP.IncCounter("EssentialFox3",0) )

Response.Write( oVFP.IncCounter("EssentialFox4",0) )

%>

 

 

 

Classic ASP

ASP.NET

Plain COM

110 rps

78 rps

COM+ Component

48 rps

58 rps

 

Notice how the performance of classic ASP stays almost stable even with the additional COM calls- it appears that most of the overhead is in the creation of the object with very little overhead for additional method calls. Calling methods is fairly fast and doesn't affect performance much. With ASP.NET however, repeated COM calls cause additional overhead as code is crossing the managed to unmanaged boundaries on each COM method call or property access performance drops significantly for every added COM call.

 

But look at the COM+ numbers. In all scenarios running a COM+ component seems to slow down performance significantly and the difference between ASP and ASP.NET actually switched to an advantage for .NET. But performance stays very steady regardless of accesses to the component and in fact ASP.NET is faster with COM+ components.

 

Regardless, of the slower numbers with .NET keep in mind that the even the lower numbers are rather acceptable in terms of performance! If you can sustain even the slowest 40 requests a second on a single server is still close to 3.5 million hits in 24 hour period, which is impressive for the test hardware it's running (midrange desktop machine w/o fully optimized hardware).

 

But regardless it's important to understand that there is a performance penalty to pay when using ASP.NET over classic ASP. In fact, if you're building an application that's primarily using COM objects to perform their business logic or even have applications that serve all your HTML content from within a COM object (some backend engines use ASP as a Web front end) classic ASP may actually be a much better choice for performance reasons. Comparing plain COM operation with lots of method calls to COM components you can see that performance can multiple times faster with classic ASP.

 

Keep in mind that this test mainly tests for COM call overhead not the actual speed of the code running. As you run requests that run more code inside the VFP component the call overhead becomes less important. But if you're accessing lots of properties or short quick method calls (which often is typical for business object access) you will see performance issues similar to this test.

Summary

.NET's COM interop features make it possible for ASP.NET to easily talk to Visual FoxPro COM components so you can leverage your existing Fox code in .NET. Although the process is not complicated it does require a few extra steps that you did not have to deal with in classic ASP. The most problematic issues are dynamic properties which require using .NET Reflection to dynamically access properties and methods of these custom objects. In addition, although .NET addresses many of classic ASPs shortcomings with native code, using COM with ASP.NET leaves all of the administration and development headaches of its predecessor in place. You still will need to shut the Web Service or application down to update a COM component and deal with the full COM/COM+ registration process which is next to impossible to administer without downtime on a running server.

 

It's also important to understand that there is a performance penalty to pay when using ASP.NET over classic ASP. In fact, if you're building an application that's primarily using COM objects to perform their business logic or even have applications that serve all your HTML content from within a COM object (some backend engines use ASP as a Web front end) classic ASP may actually be a much better choice for performance reasons. Comparing plain COM operation with lots of method calls to COM components you can see that performance can be multiple times faster with classic ASP.

 

It's clear that COM interop in .NET is a backwards compatibility feature meant to be a stop over solution until code can be migrated to native .NET code. But don't let these issues discourage you – the key features needed to integrate Visual FoxPro code are available and although it might take an extra step being able to integrate the power of VFP into ASP.NET is worth it.

 

 

 

 

 

 

 

 

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

 

 

 

 

 

 

 

 

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

 

 

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