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.
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.
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 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?
First you'll need a timesheet transaction table. The structure of this table is presented in Listing 22.1.
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.
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:
ADMIN | |
SALES | |
NEEDS | |
TECHSUPP | |
SPEC | |
PROGRAM | |
SYSTEST | |
USERDOC | |
TECHDOC | |
CODEREV | |
REPORT |
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.
*-- 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 **************************************************
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.
*-- 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()
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:
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.
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.
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.
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.
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).
*-- 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).
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.
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.
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.
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)
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.
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.
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.
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.
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.
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.
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.
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.
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. |
All members not specifically designed for use by a client program have been protected and are no longer visible.
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.
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.