Special Edition Using Visual FoxPro 6


Chapter 22

Creating COM Servers with Visual FoxPro


What do you Need COM Servers For?

Chapters 20 and 21 discussed working with COM servers within Visual FoxPro. However, Visual FoxPro as a COM client is only the beginning of the story. Visual FoxPro adds an incredible amount of power and flexibility to your application development arsenal by enabling you to easily create COM servers.

You're probably wondering why you need COM servers at all. Applications today are increasing in scope. Instead of applications being deployed on a single machine or even on a local area network (LAN), they are increasingly being deployed on an enterprisewide scale via the Internet or the corporate intranet. With applications now being accessed by larger numbers of users than ever before, a new strategy for deploying these applications is necessary.

Multi-tiered client/server applications are the means by which applications can be globally deployed with a minimum of fuss and bother.

A multi-tiered application is an application where the application is created in logical pieces. The user sees the front end, or the GUI. The data is stored on the back end. In the middle is a layer that is designed to enforce the business rules and handle the communications between the front and back ends.

The front end can be created in anything you want. Microsoft has been advocating the use of browser-based front ends for a few reasons. First of all, if your GUI is written using HTML, Dynamic HTML (DHTML), ASP, and the like, deploying the application is now a snap. In effect, all the client has to do is point to the right Web page and the application is deployed. What could be simpler? This makes it even easier to upgrade the application. In addition, using a browser-based front end has the benefit of reducing training time as more and more people are familiar with using browsers nowadays.

The back end can be anything you want. Visual FoxPro, SQL Server, you name it. It doesn't matter.

The mechanism used for the inter-layer communications is COM. The front end instantiates the middle tier, which passes data back and forth between the front end and the back end.

So, the idea is this: In a multi-tiered client/server application, the middle tier consists of COM servers. When push comes to shove, no one does data better than Visual FoxPro. Visual FoxPro is also a premier object-oriented development environment. When all is said and done, Visual FoxPro is the natural choice for creating the middle tier.

You'll see an example of Visual FoxPro as the middle tier in a three-tiered client/server application later in this chapter. In the meantime, focus on what it takes to create a COM server in Visual FoxPro. As you'll see, it is almost laughably simple.

A Simple COM Server Example

This chapter tackles a simple example of creating a COM server in Visual FoxPro. The following sections walk you through the task description, design plan, table creation, and initial code for this example.

The Task

Many organizations use timesheets, which are used to record time spent on projects so that clients can be billed.

Right now, the users enter their time using a custom Excel spreadsheet. Others have designed a special interface using Visual Basic. The task is to create a generic program that the users of these programs can use to populate the back-end tables (in this case, Visual FoxPro tables).

The Design

The first step is to design the solution to this task as if you were using Visual FoxPro in a single-tier application. In this case, the design is to create a single class with properties that match the fields in the timesheet transaction table. A method clears the properties (such as appending a blank record) and another method saves the data to the table.

Simple, right?

The Tables

First you'll need a timesheet transaction table. The structure of this table is presented in Listing 22.1.


Listing 22.1  22CODE01.DBF-Structure for Timesheet Transaction Table
Structure for table:          C:\CHAPTER22\TSTRANS.DBF
Number of data records:       0    
Date of last update:          09/08/98
Memo file block size:         64
 Code Page:                   1252 
   Field   Field Name      Type       Width  Dec      Index Collate  Nulls
       1   CEMPLOYEE       Character  10              Asc   Machine     No
       2   DDATE           Date        8              Asc   Machine     No
       3   CCLIENT         Character  45                                No
       4   CSERVICE        Character  10                                No
       5   MDESC           Memo        4                                No
       6   NHOURS          Numeric    10     2                          No
       7   NRATE           Numeric    10     2                          No
       8   NAMOUNT         Numeric    10     2                          No
** Total **                                                            108

Employees do not enter the rate charged for their services. In this mythical firm, a different rate is charged for each different service. So, you need a table for this. The Services rate table structure is shown in Listing 22.2.


Listing 22.2  22CODE02.DBF-Structure for Services Rate Table
Structure for table:          C:\CHAPTER22\SERVICES.DBF
Number of data records:       11   
Date of last update:          09/08/98
 Code Page:                   1252 
Field   Field Name      Type       Width  Dec     Index   Collate  Nulls
    1   CSERVICE        Character  10                                 No
    2   NRATE           Numeric    10     2                           No
** Total **                                                           21

Before I go into the code model for this, here's the list of services and the rates charged:

Service
Rate
ADMIN
0.00
SALES
0.00
NEEDS
100.00
TECHSUPP
65.00
SPEC
100.00
PROGRAM
85.00
SYSTEST
85.00
USERDOC
80.00
TECHDOC
90.00
CODEREV
100.00
REPORT
75.00

The Code

The plan is to create a single class that models the table. The class would have one property for each field in the table. The idea is for the user to populate each property. After the properties have been filled, the data can be saved.

There are two issues that have not yet been addressed. First of all, there are some calculations that need to take place. Based on the service, a rate needs to be selected. After the rate has been selected, the billing amount is calculated by multiplying the number of hours entered by the rate.

This is accomplished by attaching assign methods to the service, rate, and hour properties. When a service is specified, the rate is looked up and placed in the rate property. When the rate is populated, the rate is multiplied by the number of hours. The hours assign method does the same thing as the rate assign (after all, the user could specify either the hours or the service first).

The code as exported from the class browser is shown in Listing 22.3.


Listing 22.3  22CODE03.PRG-Source Code for the Timesheet Program That Was Exported from the Class Browser
*-- Program....: TIMESHEET.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: September 1, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...:
*-- Changes....:

**************************************************
*-- Class:        timetrans (c:\Chapter22\timesheet.vcx)
*-- ParentClass:  line
*-- BaseClass:    line
*
DEFINE CLASS timetrans AS line


     Height = 17
     Width = 100
     cclient = ("")
     cdescription = ("")
     *-- Service Code
     cservice = ("")
     *-- Billing amount
     namount = 0.00
     nrate = 0.00
     *-- The employee who did the work
     cemployee = ("")
     *-- Date of service
     ddate = {}
     *-- Hours worked
     nhours = 0.00
     Name = "timetrans"


     PROTECTED PROCEDURE cdescription_assign
          LPARAMETERS vNewVal

          *-- Our billing program only prints up to 600 characters for the
          *-- description.

          IF LEN(vNewVal) > 600
               vNewVal = LEFT(vNewVal, 600)
          ENDIF
          THIS.cDescription = m.vNewVal
     ENDPROC


     *-- Saves a transaction to the table
     PROCEDURE save
          LOCAL lnSelect
          lnSelect = SELECT()

          IF !USED("tsTrans")
               USE tstrans
          ENDIF

          SELECT tstrans
          APPEND BLANK

          WITH this
               REPLACE ;
                    cEmployee WITH .cEmployee, ;
                    dDate with .dDate, ;
                    cClient WITH .cClient, ;
                    cService WITH .cService, ;
                    mDesc WITH .cDescription, ;
                    nHours with .nHours, ;
                    nRate WITH .nRate, ;
                    nAmount WITH .nAmount
          ENDWITH

          SELECT (lnSelect)
     ENDPROC


     *-- Clears out everything for a new record to be defined
     PROCEDURE add
          WITH this
               .dDate = {}
               .cClient = ""
               .cService = ""
               .cDescription = ""
               .nHours = 0.00
               .nRate = 0
               .nAmount = 0
               .cEmployee = ""
          ENDWITH
     ENDPROC


     PROTECTED PROCEDURE cservice_assign
          LPARAMETERS vNewVal

          *-- When we get a service in, we need to get the rate so we can
          *-- do the math on it and then save it to the table.

          LOCAL lnSelect, lcOldExact
          lnSelect = SELECT()

          this.cService = vNewVal

          *-- Check that we got a character value.

          IF TYPE("vNewVal") # "C"
               IF INLIST(_vfp.startmode, 1, 2, 3)
                    this.cStatus = "cService Error"
               ELSE
                    MessageBox("cService expects a CHARACTER type value.", 16)
               ENDIF
               RETURN
          ENDIF

          IF !USED("services")
               USE services
          ENDIF

          SELECT services
          SET ORDER TO service

          lcOldExact = SET("exact")
          SET EXACT ON
          SEEK UPPER(ALLTRIM(vNewVal))

          IF !FOUND() OR EMPTY(vNewVal)
               this.nRate = 0.00
          ELSE
               this.nRate = services.nRate
          ENDIF

          SET EXACT &lcOldExact
          SELECT (lnSelect)
     ENDPROC


     PROTECTED PROCEDURE nrate_assign
          LPARAMETERS vNewVal

          *-- Calculate the billing amount for this number

          this.nRate = vNewVal

          IF TYPE("vNewVal") # "N"
               IF INLIST(_vfp.startmode, 1, 2, 3)
                    this.cStatus = "Rate Error"
               ELSE
                    MessageBox("nRate expects a NUMERIC type value.", 16)
               ENDIF
          ELSE
               this.nAmount = this.nRate * this.nHours
          ENDIF
     ENDPROC


     PROTECTED PROCEDURE nhours_assign
          LPARAMETERS vNewVal

          *-- Calculate the billing amount for this number

          this.nHours = vNewVal

          IF TYPE("vNewVal") # "N"
               IF INLIST(_vfp.startmode, 1, 2, 3)
                    this.cStatus = "Hours Error"
               ELSE
                    MessageBox("nHours expects a NUMERIC type value.", 16)
               ENDIF
          ELSE
               this.nAmount = this.nRate * this.nHours
          ENDIF
     ENDPROC


ENDDEFINE
*
*-- EndDefine: timetrans
**************************************************

A Quick Status Report

At this point, you have a class that does what you want. To test it, you can write a simple program like the one shown in Listing 22.4 that will prove that the class indeed does work. If you run the program, you will see that the code does populate the tsTrans table with the information placed in the properties as well as the information gleaned from the services table and the amount calculation.


Listing 22.4  22CODE04.PRG-Structure for Timesheet Transaction Table
*-- Program....: TESTIT.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: September 2, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...:
*-- Changes....:


SET CLASSLIB TO TimeSheet
ox = CreateObject("TimeTrans")

ox.Add()
ox.cEmployee = "MB"
ox.dDate = {^1998-08-01}
ox.cClient = "SAMS"
ox.cService = "PROGRAM"
ox.cDEscription = "Work on the book"
ox.nHours = 8.25
ox.Save()

ox.Add()
ox.cEmployee = "MB"
ox.dDate = {^1998-08-02}
ox.cClient = "SAMS"
ox.cService = "USERDOC"
ox.cDEscription = "Write on the book"
ox.nHours = 8
ox.Save()

Creating the COM Server

Now that you have a working class, you are ready to make a COM server out of it. The process for this is very simple:

  1. Modify the class.
  2. Display the Class Info dialog (choose Class, Class Info) shown in Figure 22.1.
  3. Check OLE Public.
  4. Close the dialog.
  5. Save the class.

Figure 22.1 : The Class Info dialog enables you to expose a class.

By checking the OLE Public check box, the class has been marked as exposed for OLE purposes.

The only thing left to do is to generate an executable or .DLL so that the server is registered. In order to create the .EXE or .DLL, you need a project. The project, which I have called TS.PJX, needs the class library and a "stub program," a .PRG that you can call the "main program" so that the executable or .DLL can be built.

In this case, the main program is called MAIN.PRG and has only one line of code:

*-- Program....: MAIN.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: September 2, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...:
*-- Changes....:

RETURN

You are now ready to build the .EXE or .DLL.

Build an .EXE or a .DLL?

What's the difference between building an .EXE or a .DLL? The basic difference lies in whether the OLE server will run as an InProc server or not. An InProc server is a server that runs within the same memory space as the application that calls it, and is implemented as a .DLL. An OutProc server runs in its own memory space and is implemented as an .EXE file.

I prefer to build my servers as .EXEs for a couple of reasons:

On the other hand, an .EXE can run a little slower than a DLL because the operating system has to open a pipe between the memory spaces. Chapter 12, "Organizing Components of an Application into a Project," describes how to use the Project Manager to build either an .EXE or a COM .DLL.

Back on Track

Okay, you have built the .EXE. The process of building the server generates the GUIDs and registers the server in the Registry. After the server is built, you will find the following entries in the Registry:

VB5SERVERINFO
VERSION=1.0.0

HKEY_CLASSES_ROOT\ts.timetrans = timetrans
HKEY_CLASSES_ROOT\ts.timetrans\NotInsertable
HKEY_CLASSES_ROOT\ts.timetrans\CLSID = {00F32181-47CC-11D2-B30B-F34CEA44F62D}
HKEY_CLASSES_ROOT\CLSID\{00F32181-47CC-11D2-B30B-F34CEA44F62D}
 = timetrans
HKEY_CLASSES_ROOT\CLSID\{00F32181-47CC-11D2-B30B-F34CEA44F62D}
\ProgId = ts.timetrans
HKEY_CLASSES_ROOT\CLSID\{00F32181-47CC-11D2-B30B-F34CEA44F62D}
\VersionIndependentProgId = ts.timetrans
HKEY_CLASSES_ROOT\CLSID\{00F32181-47CC-11D2-B30B-F34CEA44F62D}
\LocalServer32 = ts.exe /automation
HKEY_CLASSES_ROOT\CLSID\{00F32181-47CC-11D2-B30B-F34CEA44F62D}
\TypeLib = {00F32183-47CC-11D2-B30B-F34CEA44F62D}
HKEY_CLASSES_ROOT\CLSID\{00F32181-47CC-11D2-B30B-F34CEA44F62D}
\Version = 1.0
HKEY_CLASSES_ROOT\INTERFACE\{00F32182-47CC-11D2-B30B-F34CEA44F62D}
 = timetrans
HKEY_CLASSES_ROOT\INTERFACE\{00F32182-47CC-11D2-B30B-F34CEA44F62D}
\ProxyStubClsid = {00020424-0000-0000-C000-000000000046}
HKEY_CLASSES_ROOT\INTERFACE\{00F32182-47CC-11D2-B30B-F34CEA44F62D}
\ProxyStubClsid32 = {00020424-0000-0000-C000-000000000046}
HKEY_CLASSES_ROOT\INTERFACE\{00F32182-47CC-11D2-B30B-F34CEA44F62D}
\TypeLib = {00F32183-47CC-11D2-B30B-F34CEA44F62D}
HKEY_CLASSES_ROOT\INTERFACE\{00F32182-47CC-11D2-B30B-F34CEA44F62D}
\TypeLib\"Version" = 1.0


; TypeLibrary registration
HKEY_CLASSES_ROOT\TypeLib\{00F32183-47CC-11D2-B30B-F34CEA44F62D}
HKEY_CLASSES_ROOT\TypeLib\{00F32183-47CC-11D2-B30B-F34CEA44F62D}
\1.0 = ts Type Library
HKEY_CLASSES_ROOT\TypeLib\{00F32183-47CC-11D2-B30B-F34CEA44F62D}
\1.0\0\win32 = ts.tlb
HKEY_CLASSES_ROOT\TypeLib\{00F32183-47CC-11D2-B30B-F34CEA44F62D}
\1.0\FLAGS = 0

Notice that all this information is in a file called a VBR file, which is automatically generated when the .EXE is built.

Single Versus Multiple Instancing

After the EXE has been built and the server registered for the first time, you have one more thing to do: specify whether the server is single instancing only or multiple instancing.

This information is accessed from the Servers tab on the Project Information dialog box (see Figure 22.2).

Figure 22.2 : You can specify how instancing of the server object will be handled.

If you looked at this dialog before you built the .EXE, the servers in the project would not show up because they wouldn't yet have been registered.

The decision to be made now is how the instancing of the object will be handled. There are three options:

CAUTION
Each server (an .EXE can have more than one) has to be set separately.

After you have all your servers set the way you want them, rebuild the .EXE or .DLL. Now you're ready to test it.

Testing the Server

The only thing left to do is test the new server. Because you are working in Visual FoxPro anyway, and Visual FoxPro is itself a wonderful COM client, there is little work necessary to create a test program. A simple modification to the test program you saw earlier will do (see Listing 22.5).


Listing 22.5  22CODE05.PRG-Program That Tests the COM Server
*-- Program....: TESTCOM.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: September 2, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...:
*-- Changes....:

*-- This test program tests the COM server.

ox = CreateObject("ts.TimeTrans")

ox.Add()
ox.cEmployee = "MB"
ox.dDate = {^1998-08-01}
ox.cClient = "SAMS"
ox.cService = "PROGRAM"
ox.cDEscription = "Work on the book"
ox.nHours = 8.25
ox.Save()

ox.Add()
ox.cEmployee = "MB"
ox.dDate = {^1998-08-02}
ox.cClient = "SAMS"
ox.cService = "USERDOC"
ox.cDEscription = "Write on the book"
ox.nHours = 8
ox.Save()

Notice that the only change to this program is the CreateObject() call, which is clearly a call to a COM server (indeed, the one just built).

Testing the Server Outside Visual FoxPro

So far, things are going fantastically well. Nothing has gone wrong. The server, as promised, has been a breeze to put together. The Visual FoxPro class shown earlier has become a sophisticated OLE server with almost no work. You were even able to run it without any problems in Visual FoxPro. All you have left to do to put this to the real test is to test the server in another environment. For this, let's use Excel.

The Excel Development Environment

It's not my purpose to get deep into the Excel development environment. There are enough books out there to handle that. However, in order to understand what I am about to do, it will help to note a few quick things.

First, when creating an Excel macro or module that will use a COM server, you need to make sure that the VBA development environment knows about the COM server. You do this by telling it to reference the type library for the server you are using.

First, you obviously need to bring up the Visual Basic Editor. Then, after choosing Tools, References, make sure that ts Type Library is checked.

Second, in Visual Basic, unlike Visual FoxPro, you can specifically tell the language the data type of the memory variable you are using. This is known as strong typing. By telling the language the type of memory variable you are using, the compiler can do more error checking for you.

In addition, you can tell Visual Basic that the data type of a memory variable is a specific type of object. In this case, the intellisense feature kicks in and actually pops up a list of available properties and methods for you to choose from as you type.

The Excel Code

Now that you have covered a few details about Excel, the code for the Excel module is crystal clear. Here it is:

Sub TestComServer()
    '*-- Program....: TestComServer
    '*-- Version....: 1.0
    '*-- Author.....: Menachem Bazian, CPA
    '*-- Date.......: September 2, 1998
    '*-- Project....: Using Visual FoxPro 6 Special Edition
    '*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights
    '*-- Reserved.
    '*-- Compiler...: Excel 97 SR-1
    '*-- Abstract...:
    '*-- Changes....:
 
    '*-- This test program tests the COM server.

    Dim ox As New ts.timetrans
    Set ox = CreateObject("ts.TimeTrans")
 
    ox.Add
    ox.cemployee = "MB"
    ox.ddate = #8/1/98#
    ox.cclient = "SAMS"
    ox.cservice = "PROGRAM"
    ox.cdescription = "Work on the book - From Excel"
    ox.nhours = 8.25
    ox.Save
 
    ox.Add
    ox.cemployee = "MB"
    ox.ddate = #8/2/98#
    ox.cclient = "SAMS"
    ox.cservice = "USERDOC"
    ox.cdescription = "Write on the book - From Excel"
    ox.nhours = 8
    ox.Save
End Sub

A close look at the macro shows that there is an incredible correspondence between the Excel macro and the Visual FoxPro version. The only differences are extremely minor. A comment starts with a single quote ('). Dates are encased in pound signs (##). The DIM and CREATEOBJECT statements are slightly different. Beyond that, the code is straight from Visual FoxPro.

Close, But No Cigar

And the results? Well, the results in your case will depend, but in most cases, the macro will not run. In all probability, an Open dialog will appear on your Windows desktop asking you to locate SERVICES.DBF (in my case, it said C:\My Documents\Services.DBF Not found).

The reason for this is simple. When a Visual FoxPro COM server is loaded, the default directory is not the directory where the .EXE is. Sometimes, the default directory is the SYSTEM32 directory; other times, it is the default directory used by the COM client (as it is in this case with Excel). In any event, the lesson learned here is that you cannot be sure of the default directory, path, or anything like that. You need to set it yourself.

How do you set it? Well, the easiest way is to add a property to the class stating where the data files are hidden and tack that value onto the USE statements. This method, although kludgy, does work for simple COM servers where you are in total control of the environment. However, once you get into the question of network drives (which can be mapped differently from machine to machine), a more generic means is necessary.

Fortunately, the Windows API has the answer in the form of GetModuleFileName(), a function in the Win32 API.

Basically, GetModuleFileName() looks at the current running application and gives you the name of the file (.EXE) it is running. So, all you have to do is DECLARE the .DLL, send it the proper parameters, and you have your information. It's simple, and looks like this:

DECLARE INTEGER GetModuleFileName in win32api Integer,String @,Integer

GetModuleFileName() returns an integer, but what you want is the second parameter, which is the name of the .EXE that is running. Notice that the second parameter is passed by reference, and thus will get the value you want.

So, here's what you need to do in a program:

LOCAL lcFileName, lnRetVal
lcFileName = SPACE(400)
DECLARE INTEGER GetModuleFileName in win32api Integer,String @,Integer
lnRetVal = GetModuleFileName(0, @lcFileName, 400)

lcFileName is a placeholder of blanks. When passed to the function, the value of the variable is replaced with the name of the .EXE (with full path), padded out to 400 characters. The final parameter tells GetModuleFileName() how large the buffer is.

The return value tells how large the actual filename is. So, to get the full filename and path, all you need to do is

LcFileName = LEFT(lcFileName, lnretVal)

After you have the fully qualified filename, you can strip off the filename and are left with the directory where the .EXE is.

Now, all this is well and good if you are dealing with an .EXE. What about a .DLL? A .DLL is a little different because it runs in the same process space as the COM client. You first need to get a handle to the .DLL process and then you can call GetModuleFileName().

Here's how the whole mess works out in code. Keep in mind that this program (called simply TEST.PRG) would be placed into a class as a method:

*-- Program....: TEST.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: September 3, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...: Gets the location for a COM server
*-- Changes....:

*-- This code is meant to be placed in a method of a COM server. It
*-- assumes the presence of a property called cServerName which has the
*-- name of the .DLL (if this is a DLL)

LOCAL lcFileName, lnLength
lcFileName = space(400)
DECLARE INTEGER GetModuleFileName in win32api Integer,String @,Integer

*-- If we are starting up as a DLL, we need to first use GetModuleHandle to
*-- get a module handle for the DLL, otherwise we can use 0 as the module
*-- handle.

*-- It's important to understand that the first parameter in GetModuleFileName
*-- is the HANDLE (an integer) of the process we are trying to identify.
*-- The default value for this is the module that initiated the calling
*-- process. Now, if we are dealing with an out of proc server, then
*-- it is running in its own process. No problem. A DLL, on the other hand,
*-- runs in another process so we need the name of the DLL in order to get
*-- the module handle to call GetModuleFileName.
*--
*-- This.srvname is populated either by the developer or by some other process.

IF _vfp.startmode = 3 && Visual FoxPro started to service an In Process COM Server
     DECLARE INTEGER GetModuleHandle in win32api String
     lnLength = Getmodulefilename( ;
                         GetModuleHandle(this.cSrverName + ".dll"), ;
                         @lcFileName, len(lcFileName))
ELSE
     lnLength = Getmodulefilename(0, @lcFileName, len(lcFileName))
ENDIF

lcFileName = LEFTC(lcFileName, lnLength)
RETURN LEFTC(lcFileName, RATC('\',lcFileName) -1)

Rebuilding the Server

You need to know one more thing before you can rebuild the server. The server program is not released from memory when it aborts (as it did when I ran the server from Excel). You need to manually end the task through the Task Manager (Windows NT) or the Close Program dialog (Windows 95/98).

If your process is not running and you still cannot rebuild the server, see whether Excel or any other application is open and referencing the type library. If the type library is referenced by an open application (for instance, a Visual Basic project), it cannot be overwritten.

Smoothing out a Few Wrinkles

There are a few specifics that should be noted that I did not do in the version of the class you have been testing. They are explained in the following sections.

Managing the Tables

First of all, it is not a good idea for the server to open a file and keep it open. Given the stateless nature of COM servers (they could be used by anyone at any time), it's a good idea to keep things extra clean. Open the tables only when you need them and close them soon after.

Deciding Which Class to Base It On

Which class should a COM server be based on? Well, in truth you could use just about any class. I use the Line class because it purports to have the lowest overhead.

Protecting Properties and Methods

You might have noticed that the assign methods in the TimeTrans class were all scoped as protected methods. A protected method is one that can only be used within the class and is not visible outside the class.

A general rule to follow is not to expose anything to the outside world that is not needed there. This means that all properties, methods, and events that are irrelevant to the COM client's interaction with the server should be protected or hidden.

With a Line class, there are a host of properties, methods, and events that are irrelevant. They should all be protected in a real-life COM server.

Dealing with Modals and Errors

A COM server should never stop. That's an axiom. In order to make sure that it doesn't, you need to make sure that you have a good ON ERROR routine (or Error method) that traps all errors and deals with them gracefully.

Sys(2335)

Visual FoxPro has a nifty little function called SYS(2335). This function toggles something called Unattended Server Mode, which tells Visual FoxPro that modal states-such as messagebox() calls-are strictly verboten. You turn it on using SYS(2335,0) and off using SYS(2335,1). When Unattended Server Mode is on, any attempt at a modal state in the application generates an error that can be trapped in your ON ERROR routine.

For the record, SYS(2335) is only applicable for .EXE COM servers. A .DLL is always in unattended mode. Issuing SYS(2335) without the second parameter returns the current setting.

The Final Version of the Server

After you correct your server for the issues mentioned above, here's what you come up with:

*-- Program....: TIMESHEET2.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: September 3, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...:
*-- Changes....:

**************************************************
*-- Class:        newtimetrans (c:\chapter22\timesheet.vcx)
*-- ParentClass:  line
*-- BaseClass:    line
*
DEFINE CLASS newtimetrans AS line OLEPUBLIC


     Height = 17
     Width = 100
     cclient = ("")
     cdescription = ("")
     *-- Service Code
     cservice = ("")
     *-- Billing amount
     namount = 0.00
     nrate = 0.00
     *-- The employee who did the work
     cemployee = ("")
     *-- Date of service
     ddate = {}
     *-- Hours worked
     nhours = 0.00
     Name = "newtimetrans"
     PROTECTED csrvname
     PROTECTED cscriptdir

     *-- Returns the path to the data.
     PROTECTED cdatapath
     PROTECTED height
     PROTECTED width
     PROTECTED name
     PROTECTED baseclass
     PROTECTED bordercolor
     PROTECTED borderstyle
     PROTECTED borderwidth
     PROTECTED class
     PROTECTED click
     PROTECTED cloneobject
     PROTECTED colorsource
     PROTECTED comment
     PROTECTED dblclick
     PROTECTED destroy
     PROTECTED drag
     PROTECTED dragdrop
     PROTECTED dragicon
     PROTECTED dragmode
     PROTECTED dragover
     PROTECTED drawmode
     PROTECTED enabled
     PROTECTED error
     PROTECTED helpcontextid
     PROTECTED init
     PROTECTED lineslant
     PROTECTED middleclick
     PROTECTED mousedown
     PROTECTED mouseicon
     PROTECTED mousemove
     PROTECTED mousepointer
     PROTECTED mouseup
     PROTECTED mousewheel
     PROTECTED move
     PROTECTED parent
     PROTECTED parentclass
     PROTECTED readexpression
     PROTECTED readmethod
     PROTECTED resettodefault
     PROTECTED rightclick
     PROTECTED saveasclass
     PROTECTED showwhatsthis
     PROTECTED tag
     PROTECTED uienable
     PROTECTED visible
     PROTECTED whatsthishelpid
     PROTECTED writeexpression
     PROTECTED writemethod
     PROTECTED zorder
     PROTECTED classlibrary
     PROTECTED addproperty
     PROTECTED olecompletedrag
     PROTECTED oledrag
     PROTECTED oledragdrop
     PROTECTED oledragmode
     PROTECTED oledragover
     PROTECTED oledragpicture
     PROTECTED oledropeffects
     PROTECTED oledrophasdata
     PROTECTED oledropmode
     PROTECTED olegivefeedback
     PROTECTED olesetdata
     PROTECTED olestartdrag


     PROTECTED PROCEDURE cdescription_assign
          LPARAMETERS vNewVal

          *-- Our billing program only prints up to 600 characters for the
          *-- description.

          IF LEN(vNewVal) > 600
               vNewVal = LEFT(vNewVal, 600)
          ENDIF
          THIS.cDescription = m.vNewVal
     ENDPROC


     *-- Saves a transaction to the table
     PROCEDURE save
          LOCAL lnSelect
          lnSelect = SELECT()

          IF !USED("tsTrans")
               USE this.cDataPath + "tstrans"
          ENDIF

          SELECT tstrans
          APPEND BLANK

          WITH this
               REPLACE ;
                    cEmployee WITH .cEmployee, ;
                    dDate with .dDate, ;
                    cClient WITH .cClient, ;
                    cService WITH .cService, ;
                    mDesc WITH .cDescription, ;
                    nHours with .nHours, ;
                    nRate WITH .nRate, ;
                    nAmount WITH .nAmount
          ENDWITH

          SELECT (lnSelect)
          USE IN tsTrans
     ENDPROC


     *-- Clears out everything for a new record to be defined
     PROCEDURE add
          WITH this
               .dDate = {}
               .cClient = ""
               .cService = ""
               .cDescription = ""
               .nHours = 0.00
               .nRate = 0
               .nAmount = 0
               .cEmployee = ""
          ENDWITH
     ENDPROC
     PROTECTED PROCEDURE cservice_assign
          LPARAMETERS vNewVal

          *-- When we get a service in, we need to get the rate so we can
          *-- do the math on it and then save it to the table.

          LOCAL lnSelect, lcOldExact
          lnSelect = SELECT()

          this.cService = vNewVal

          *-- Check that we got a character value.

          IF TYPE("vNewVal") # "C"
               IF INLIST(_vfp.startmode, 1, 2, 3)
                    this.cStatus = "cService Error"
               ELSE
                    MessageBox("cService expects a CHARACTER type value.", 16)
               ENDIF
               RETURN
          ENDIF

          IF !USED("services")
               USE (this.cDataPath + "services")
          ENDIF

          SELECT services
          SET ORDER TO service

          lcOldExact = SET("exact")
          SET EXACT ON
          SEEK UPPER(ALLTRIM(vNewVal))

          IF !FOUND() OR EMPTY(vNewVal)
               this.nRate = 0.00
          ELSE
               this.nRate = services.nRate
          ENDIF

          SET EXACT &lcOldExact

          USE IN services
          SELECT (lnSelect)
     ENDPROC
     PROTECTED PROCEDURE nrate_assign
          LPARAMETERS vNewVal

          *-- Calculate the billing amount for this number

          this.nRate = vNewVal

          IF TYPE("vNewVal") # "N"
               IF INLIST(_vfp.startmode, 1, 2, 3)
                    this.cStatus = "Rate Error"
               ELSE
                    MessageBox("nRate expects a NUMERIC type value.", 16)
               ENDIF
          ELSE
               this.nAmount = this.nRate * this.nHours
          ENDIF
     ENDPROC
     PROTECTED PROCEDURE nhours_assign
          LPARAMETERS vNewVal

          *-- Calculate the billing amount for this number

          this.nHours = vNewVal

          IF TYPE("vNewVal") # "N"
               IF INLIST(_vfp.startmode, 1, 2, 3)
                    this.cStatus = "Hours Error"
               ELSE
                    MessageBox("nHours expects a NUMERIC type value.", 16)
               ENDIF
          ELSE
               this.nAmount = this.nRate * this.nHours
          ENDIF
     ENDPROC


     *-- Gets the path to the EXE which also tells us the path to the data files.
     PROTECTED PROCEDURE getpaths
          *-- Program....: GetPaths
          *-- Version....: 1.0
          *-- Author.....: Menachem Bazian, CPA
          *-- Date.......: September 3, 1998
          *-- Project....: Using Visual FoxPro 6 Special Edition
          *-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights
          *-- Reserved.
          *-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
          *-- Abstract...: Gets the location for a COM server
          *-- Changes....:

          *-- This code is meant to be placed in a method of a COM server. It
          *-- assumes the presence of a property called cServerName which has the
          *-- name of the .DLL (if this is a DLL)

          LOCAL lcFileName, lnLength
          lcFileName = space(400)
          DECLARE INTEGER GetModuleFileName in win32api Integer,String @,Integer

          *-- If we are starting up as a DLL, we need to first use
          *-- GetModuleHandle to get a module handle for the DLL, otherwise we
          *-- can use 0 as the module handle.

          *-- It's important to understand that the first parameter in
          *-- GetModuleFileName is the HANDLE (an integer) of the process we are
          *-- trying to identify. The default value for this is the module that
          *-- initiated the calling process. Now, if we are dealing with an out
          *-- of proc server, then it is running in its own process. No problem.
          *-- A DLL, on the other hand, runs in another process so we need the
          *-- name of the DLL in order to get
          *-- the module handle to call GetModuleFileName.
          *--
          *-- This.srvname is populated either by the developer or some other
          *-- process.

          IF INLIST(_vfp.startmode, 0, 4)
               RETURN CURDIR()
          ENDIF

          IF _vfp.startmode = 3 && Visual FoxPro started to service an In Process
               COM Server
               DECLARE INTEGER GetModuleHandle in win32api String
               lnLength = Getmodulefilename( ;
                                   GetModuleHandle(this.cSrverName + ".dll"), ;
                                   @lcFileName, len(lcFileName))
          ELSE
               lnLength = Getmodulefilename(0, @lcFileName, len(lcFileName))
          ENDIF

          lcFileName = LEFTC(lcFileName, lnLength)

          RETURN LEFTC(lcFileName, RATC('\',lcFileName))
     ENDPROC
     PROTECTED PROCEDURE cdatapath_access
          *To do: Modify this routine for the Access method
          *-- The idea here it to calculate the datapath if it has not yet
          *-- been calculated

          IF EMPTY(this.cDataPath)
               this.cDataPath = this.GetPaths()
          ENDIF

          RETURN THIS.cDataPath
     ENDPROC


     PROCEDURE Error
          LPARAMETERS nError, cMethod, nLine

          #DEFINE _TAB CHR(9)

          *-- Just write this to an error text file

          IF !FILE(this.cDataPath + "Errors.TXT")
               lnHandle = FCREATE(this.cDataPath + "Errors.TXT")
          ELSE
               lnHandle = FOPEN(this.cDataPath + "Error.TXT")
          ENDIF

          lcErrorStr = TTOC(DATETIME()) + _TAB + ;
                              TRANSFORM(nError, "9999") + _TAB + ;
                              cMethod + _TAB + ;
                              TRANSFORM(nError, "999999")

          FPUTS(lnHandle, lcErrorStr)

          =FCLOSE(lnHandle)
          RETURN
     ENDPROC


ENDDEFINE
*
*-- EndDefine: newtimetrans
**************************************************

There are several important items to note, as explained in the following sections.

New Error Method

The error method is designed to keep the application running at all costs. In this case, it's a simple one and doesn't really do much. You'll need to come up with a more robust error method for your applications. For now, just bear in mind that you have to have one for a COM server.

TIP
Remember that if you don't like the Error method, you can always use an ON ERROR routine.

Protected Members

All members not specifically designed for use by a client program have been protected and are no longer visible.

New cDataPath Property

The new cDataPath property has the directory that holds the data. The Access method, which is called when the property is read, calls the new GetPaths() method, which uses the GetModuleFileName() API call discussed earlier.

COM in N-Tiered Client/Server Applications

This chapter has looked at the business of creating a COM server in Visual FoxPro. You've seen an example of a COM server and why one might be created. COM servers, however, really fit right into the world of n-tiered client/server applications.

The COM server just shown is, in effect, the middle tier of a three-tiered client/server application. In this case, Visual FoxPro fills the role of the back end as well. Excel fits nicely into place in the front end.

This is the way client/server development is going. With the proliferation of the Internet, applications are now being deployed over ever-greater geographical spans. Using a browser-based front end, with ASP, DHTML, XML, XSL, or any number of other technologies at your fingertips, you have a light client that can work really well as a front end. By placing Visual FoxPro COM servers in the middle tier, you have a mix that uses the best of the available technologies in their proper places.


© Copyright, Sams Publishing. All rights reserved.