Calvin Hsia
Microsoft Corporation
October 1996
Think back to the early computer systems available only ten or twenty years ago. In these early days of computing, users could only run a single application at a time. There was no concept of multitasking. Sharing information between applications was very cumbersome. If one application processed the output of another application, then some sort of information sharing strategy had to be devised. Along came multitasking environments, such as Microsoft® Windows®, that allowed multiple applications to run simultaneously. Users felt more powerful because they could run a spreadsheet and their accounting program simultaneously, perhaps cutting/pasting data from one to the other. Sharing data between applications became easier.
As computer operating systems evolved, OLE 1.0 was introduced, in which users could actually "embed" data objects created by other applications right into their main application. When a user opens a Word document and double-clicks on the embedded Microsoft Excel object, Excel starts up and allows the user to interact with the data. However, this implementation meant that Excel would start as its own separate application, and the user would see both Excel and Word on the screen at the same time. Furthermore, the Word document’s representation of the Excel spreadsheet would be marked with hash marks, indicating that it was being edited by Excel via OLE. This early attempt at allowing users to create "compound documents" was effective, but ungainly.
Along came OLE 2.0, which allows OLE In-Place Activation. This made the Excel embedded object come alive in the same Application Window as the Word object, with the menus and toolbars being "merged." The concept of the "compound document" is being further propagated because the document has a single "merged" editing environment.
These evolutionary steps in compound document architecture really help the interactive user to be more productive with the operating environment. Users can run multiple applications simultaneously and hence realize productivity gains. However, application developers also have seen an evolution in application programmability, allowing application writers the ability to programmatically control other applications.
With Windows, a technology called DDE, or Dynamic Data Exchange was introduced. This allowed programmers to write programs in a particular programming language that could communicate and command some other applications to do their bidding. This concept of a programmer writing a Controller or Client that uses the services of a Server was cumbersome with DDE. The programmer would have to know the programming languages of both objects, would have to create a DDE callback routine, and would have to have intimate knowledge of the server applications.
Another feature of OLE 2, OLE Automation, addresses the shortcomings of DDE and introduces a new level of inter-application communication. OLE Automation allows the application developer to allow other applications to "talk" to it programmatically in a standard way. The programmer designs an "interface” through which an OLE Automation controller can invoke routines and change properties on the OLE Automation server.
An application that can be an OLE Automation Server multiplies its usefulness to the modern computer environment. A standalone application like Excel has many graphing, mathematical, statistical, and financial functions. Other applications can take advantage of these functions simply by programmatically calling Excel. The end user of the application doesn't even need to know that Excel is being used.
Of course, in order to use an OLE server object, an application must be able to talk to it as an OLE Automation controller. Visual FoxPro™ version 3.x, Microsoft Excel, and Visual Basic® are all OLE Automation controller capable.
Several examples follow using Visual FoxPro as an OLE Automation controller. Most of these will work with Visual FoxPro 3.0 and Visual FoxPro 5.0.
PUBLIC ox
ox=CreateObject("Excel.application")
ox.visible=.t.
ox.windowstate=1
ox.left = 10
ox.application.top = 10
ox.workbooks.add
FOR i = 1 TO 3
FOR j= 1 to 5
ox.cells[i,j].value = I * 10 + j
ENDFOR
ENDFOR
ox=CreateObject("word.basic")
ox.appshow
ox.filenew
ox.insert("test")
ox.filesaveas("c:\t.txt")
If you're feeling lonely, you can send mail to yourself using this code:
ox= createobject("mapi.session")
?ox.logon("calvinh")
?ox.name,ox.class,ox.operatingsystem,ox.version
msg = ox.outbox.messages.add
msg.subject = "testsubject"
msg.text = "Don't be lonely"
recip = msg.recipients.add
recip.name = "calvinh"
?recip.resolve
msg.send(.t.,.f.)
?ox.logoff()
You can also create automatic message readers and generators via OLE Automation, and you can publish mail messages and folders over the intranet so that a client using a Web browser can view them.
ox=CreateObject("InternetExplorer.Application")
ox.visible=.t.
ox.navigate("http://www.msn.com")
If you don't like Internet Explorer or Netscape Browser, create your own in a Visual FoxPro form with just a few lines of code:
PUBLIC ox
ox = CreateObject("MyInternetExplorer")
ox.visible = .t.
ox.oWeb.navigate(GETENV("computername")) && insert your URL here
DEFINE CLASS MyInternetExplorer AS form
ADD OBJECT oWeb AS CWeb
caption = "My Internet Explorer"
PROCEDURE init
THISFORM.LEFT = 0
THISFORM.WIDTH = SYSMETRIC(1) * 3/4
THISFORM.height = SYSMETRIC(2)*3/4
THIS.Resize
PROCEDURE resize
This.oWeb.resize
ENDDEFINE
DEFINE CLASS CWeb AS olecontrol
oleclass = "Shell.Explorer.1"
PROCEDURE Resize
THIS.Width = THISFORM.Width - 10
THIS.Height = THISFORM.Height - 10
PROCEDURE Refresh
nodefault
ENDDEFINE
Then you can just click on any hyperlinks and get lost on the Web. Or you can type in a command:
ox.oweb.navigate("www.msn.com")
to go to a particular URL.
Visual FoxPro 5.0 itself is an OLE automation server. This means that from any other application that can be an OLE automation controller, you can CreateObject("VisualFoxPro.application") to get an object reference to Visual FoxPro 5.0, and change properties and invoke methods.
Some applications are not capable of being an OLE automation controller, and thus are incapable of using Visual FoxPro as an OLE automation server. However, the capability to call into the Visual FoxPro automation server is in a DLL called fpole.dll, which ships with Visual FoxPro 5.0. If these applications can call 32-bit DLLs, and then they can load this DLL and control Visual FoxPro. For example, a Windows Help file or Microsoft Word can declare and use DLLs and thus use Visual FoxPro as an automation server.
As an example of using this DLL, here's some code that you can run from Visual FoxPro 3.0 (even though Visual FoxPro 3.0 is an automation controller, it also can call into 32-bit DLLs) that calls Visual FoxPro 5.0 as an automation server.
MYDLL = "fpole.dll"
clear
DECLARE integer SetOleObject in (MYDLL) string
DECLARE integer FoxDoCmd in (MYDLL) string,string
DECLARE integer FoxEval in (MYDLL) string, string @,integer
buff="this is the initial value of buffer"+space(100)
*=SetOleObject("visualfoxpro.application")
i = 1
do while !chrsaw()
mt = "?'FoxDoCmd" +str(i)+"'"
=FoxDoCmd(mt,"")
?mt
i = i + 1
enddo
=inkey(.1)
clea dlls
From Microsoft Word, you can create a WordBasic program that starts Visual FoxPro 5.0 as an automation server, opens a database, and retrieves a specific record:
Declare Sub FoxDoCmd Lib "fpole.dll"(cCommand As String, cBringToFront As String)
Sub main
FoxDoCmd "USE c:\fox40\samples\data\customer", ""
FoxDoCmd "LOCATE FOR cust_id = 'CENTC'", ""
FoxDoCmd "_CLIPTEXT = customer.company + CHR(9) + customer.contact", ""
EditPaste
End Sub
RegisterRoutine("fpole.dll","FoxDoCmd","SS")
HotSpotText!FoxDoCmd("DO (HOME() + 'myprg')","a")
You can see a sample of this in the main Visual FoxPro Help file. From the Visual FoxPro Help menu choose Sample Applications. When the Sample Applications topic appears, click on the RUN or OPEN buttons. This invokes fpole.dll to start Visual FoxPro and runs the program hlp2fox.prg in the main Visual FoxPro directory.
From Visual Test, you can write a Visual Test Basic Script that calls into Visual FoxPro to get object references and mnemonic names of Visual FoxPro objects.
Viewport Clear
DECLARE FUNCTION MessageBox LIB "user32.dll" ALIAS "MessageBoxA" (h&,m$,c$,f&) as long
DECLARE FUNCTION FoxDoCmd cdecl LIB " fpole.dll" (h$,j$) as long
DECLARE FUNCTION FoxEval cdecl LIB " fpole.dll" (h$,j$, leng&) as long
sub DoClick (x&,y&)
dim buff$,cmd$,flag%
flag% = 0
buff$ = string$(132,32)
cmd$ = "oref = sys(1270," + str$(x&) + "," + str$(y&)+")"
'print cmd$
FoxDocmd(cmd$,"")
IF DoEval$("type('oref')") = "O" then
IF DoEval$("oref.name") <> "Screen" then
FoxDoCmd("kk = sys(1272,oref)","")
IF instr(DoEval$("kk"),".") > 0 then
FoxDoCmd("kk2 = LEFT(kk,AT('.',kk)-1)","")
FoxDoCmd("AINSTANCE(mm,kk2)","")
IF DoEval$("IIF(ALEN(mm,1) > 0,'A','B')") = "A" THEN
'IF DoEval$("AINSTANCE(mm,kk2) > 0") THEN
FoxDoCmd("kk = mm[1] + SUBSTR(kk,AT('.',kk))","")
cmd$ = DoEval("kk") +".click"
print #1,cmd$
print Cmd$
FoxDoCmd(cmd$,"")
ENDIF
endif
flag% = 1
ENDIF
endif
IF flag% = 0 then
'Play "{Click 723, 356, Left}"
cmd$ = "{Click " + str$(x&) + "," + str$(y&)+", Left}"
print #1,"play " + CHR$(34) + cmd$ + chr$(34)
play cmd$
ENDIF
end sub
function DoEval$ ( s$)
dim buff$,cmd$,k&
buff$ = string$(132,32)
k& = FoxEval(s$,buff$,len(buff$))
if k& <= 0 then
MessageBox(0,buff$,"Eval error",0)
endif
DoEval = buff$
end function
The Visual FoxPro Help file has more information on how to use fpole.dll.
An in-process OLE server is simply a DLL that lives in the same process address space as the OLE client. An out-of-process server lives in its own process space, and thus is typically an .exe.
An in-process server is faster than an EXE server when passing parameters back and forth, but it also can crash the host if it crashes. There is very little protection from an errant DLL server for the host. An EXE server can crash and not necessarily crash the host. Also, an EXE server can be run on a remote machine using remote automation or COM in a distributed environment (DCOM).
Because an EXE server runs in its own process, the processor gives it processor time slices to execute whatever it wants, which is typically an event loop. A DLL server depends on the OLE client to give it process time. This implies that when an EXE server presents a user interface (UI) element, such as a form, to the user, the user can navigate the form as expected. The DLL server, however, can present the form, but cannot interact with it.
Generally, this is not a problem because OLE servers are typically designed to perform some sort of server task, which has no UI elements. For example, running an OLE server that serves Web pages typically means the server lives on an unattended Web server machine.
Create a new Visual FoxPro 5.0 project called FIRST. Add a new file first.prg with the following lines:
DEFINE CLASS myfirst AS custom OLEPUBLIC
PROCEDURE INIT
MessageBox(program(),"Here in my first OLE server")
ENDDEFINE
Note that there is nothing about this program that's new except the "OLEPUBLIC" keyword. (You can also make Visual Classes OLEPUBLIC from the Class Info dialog box.) What does this do? Absolutely nothing, until you Build an .exe or .dll from the project. If you watch carefully during the build process, you'll see the message "Creating Type Library and Registering OLE Server."
The build process creates a first.tlb, first.vbr, along with your OLE Server (either first.exe or first.dll). The TLB is an OLE Typelibrary, which can be browsed using any OLE Type Library browser, such as the ones that come with the Visual FoxPro Class Browser, Microsoft Excel, Visual Basic, and Visual C++®. The .VBR file is a text file of registry entries. These are almost identical to those that were created and entered into your registry. The only difference is that file names are prepended with their full path names. This .VBR file is useful for the Setup wizard and when you want to deploy your OLE server on another machine, which might use different paths.
Now try the following from the Command window:
ox = CreateObject("first.myfirst")
*wait a few secs till the server comes up with the msgbox
* (you might have to alt-tab to the msgbox)
?ox.application.name
?ox.application.visible
?ox.
?ox.application.docmd("messagebox(home())")
?ox.application.docmd("_cliptext=home() + sys(2003) + sys(2004)")
You can add the line OX = CreateObject("myfirst") to the PRG as the first line, and then the .exe file will run just like any normal .exe. (Note: the string inside this CreateObject is the Visual FoxPro class name and not an OLE ProgID.) If it were a visual class library, you can add a Main program that does a SET CLASSLIB to the appropriate VCX and then CreateObject the object (not the OLE ProgID).
Note the last line in the sample. On my machine, it copies "C:\WINNT\SYSTEM32" to the Clipboard three times. This means that even though the OLE server is in a particular directory, OLE starts it with the WINNT\SYSTEM32 directory. This brings up an interesting problem. How does the OLE server find its component pieces, such as .DBFs, reports, and so on?
We need to determine the directory in which the OLE Server lives, because that's typically where the parts are. Then we can SET DEFAULT TO that directory, or we can SET PATH TO it. One solution is to make the INIT or LOAD method of the OLE Public class change to the right directory, with the directory name either hard coded or as a custom property on the class. This works fine on one machine, until the server is moved or installed on a different machine with a different path.
Fortunately, there are a couple WIN32 API functions available to help. For an EXE server, we can call the GetModuleFileName( ) function, which returns the full path name to the main .exe of the current process if null is passed as the first parameter. For a DLL server, we use the GetModuleHandle( ) function with the name of the DLL as the parameter to retrieve a handle to the server. We pass this handle to the GetModuleFileName( ) function to get the full path name of the DLL.
Note the use of the _VFP.StartMode property. The Help file shows the various values of this property, which indicate in which mode Visual FoxPro was started: normal, as an OLE server, or as a custom DLL or EXE OLE server:
CLOSE DATA ALL
SET TALK OFF
SET SAFETY OFF
SET EXCLUSIVE OFF
SET DELETED ON
DECLARE INTEGER GetPrivateProfileString in win32api String,String,String,;
String @, integer,string
if _vfp.startmode>1 && automation server
LOCAL buf,nlen
buf=space(400)
DECLARE INTEGER GetModuleFileName in win32api Integer,String @,Integer
* As an EXE or DLL, we need to find the home directory, not the curdir() and not
* the dir of the runtime. The classlibrary property shows this for PRGs, but not VCXs
IF _vfp.startmode = 3 && inproc dll
DECLARE INTEGER GetModuleHandle in win32api String
nlen=Getmodulefilename(GetModuleHandle(this.srvname+".dll"),@buf,len(buf))
ELSE
nlen=Getmodulefilename(0,@buf,len(buf))
ENDIF
buf = LEFTC(buf,nlen)
this.cdir = LEFTC(buf,RATC('\',buf)-1)
this.csrvname = SUBSTRC(buf,RATC('\',buf)+1)
this.csrvname = LEFTC(this.csrvname,AT_C(".",this.csrvname)-1)
endif
IF !EMPTY(this.cDir)
SET DEFAULT TO (this.cDir)
ENDIF
SET PATH TO (this.cDatapath)
use employee
Add the SAMPLES\DATA\TESTDATA.DBC to your FIRST.PJX. Add a new class to the project called MYFORM based on FORM and store it in first.vcx. Make the class OLE Public in the Class Info dialog box, and add three custom properties: cDir, cSrvName, and cDataPath. Initialize cDatapath to the directory where your testdata.dbc is, and cSrvName to FIRST. The cDir property will be initialized with the above code added to the LOAD event of the form class. Drag a couple of EMPLOYEE fields from the Project onto the class. Add the VCR class from the SAMPLES\CLASSES directory to your project, and drag an instance of the VCR button onto your form. Save the class and BUILD EXE first FROM first.
Now you've built an OLE server with the employee form. This server can be instantiated from any other OLE controller, but let’s test it from Visual FoxPro because it’s easy and familiar. In the Command window, type OX = CreateObject("myfirst.myclass"), then type OX.Application.visible=.t. This will make the Visual FoxPro run-time frame visible. OX.SHOW will make your form visible, and you can navigate around the form with the VCR buttons.
RELEASE OX or CLEAR ALL will release the server. If you had changed the ShowWindow property to 2, then the server form would be a Top Level form and the run-time Visual FoxPro desktop would not need to be shown. You can also add the line THISFORM.VISIBLE = .T. in the INIT event of the form class.
Now try BUILD DLL rather than BUILD EXE, and experiment. Note that you cannot interact with the server: when you bring the mouse over the server, you just get an hourglass. If the DLL server had the ability to interact with the user, then it would be more like an OLE or ActiveX Control. Even though you can't interact with the form, you can still give it commands from the Client: ox.application.docmd("skip") and ox.refresh will skip to the next record. ?ox.application.eval("recno()") will return the current record number, as expected.
Below is my sample .vbr file. You'll notice the presence of long strings of numbers. These are GUIDs or Globally Unique Identifiers (sometimes called UUIDs, or Universally Unique Identifiers). These are automatically generated by your computer and are guaranteed to be virtually unique, based upon the time stamp and your machine's network card, among other things.
When an OLE client application does a CreateObject, the parameter is called the ProgID or Programmatic ID. The registry is searched for the ProcID to find the ClassID, which is just a GUID. Then that ClassID is located in the registry to find more information about the server.
Visual FoxPro Custom OLE servers are self-registering. To register an EXE server, just run the .exe with the /regserver option. The /unregserver unregisters the server. For a DLL server, run the utility Regsvr32.exe with the name of the DLL as the first parameter. This will register the DLL. To unregister it, add a second parameter.
Visual FoxPro OLE automation servers require the presence of the Visual FoxPro run time on the target machine. This is vfp500.dll and vfp5enu.dll (for the English U.S. platform).
VB4SERVERINFO
HKEY_CLASSES_ROOT\first.myfirst = myfirst
HKEY_CLASSES_ROOT\first.myfirst\NotInsertable
HKEY_CLASSES_ROOT\first.myfirst\CLSID = {3DB63101-0FED-11D0-9A44-0080C70FB085}
HKEY_CLASSES_ROOT\CLSID\{3DB63101-0FED-11D0-9A44-0080C70FB085} = myfirst
HKEY_CLASSES_ROOT\CLSID\{3DB63101-0FED-11D0-9A44-0080C70FB085}\ProgId = first.myfirst
HKEY_CLASSES_ROOT\CLSID\{3DB63101-0FED-11D0-9A44-0080C70FB085}\VersionIndependentProgId = first.myfirst
HKEY_CLASSES_ROOT\CLSID\{3DB63101-0FED-11D0-9A44-0080C70FB085}\LocalServer32 = first.exe /automation
HKEY_CLASSES_ROOT\CLSID\{3DB63101-0FED-11D0-9A44-0080C70FB085}\TypeLib = {3DB63102-0FED-11D0-9A44-0080C70FB085}
; TypeLibrary registration
HKEY_CLASSES_ROOT\TypeLib\{3DB63102-0FED-11D0-9A44-0080C70FB085}
HKEY_CLASSES_ROOT\TypeLib\{3DB63102-0FED-11D0-9A44-0080C70FB085}\1.0 = first Type Library
HKEY_CLASSES_ROOT\TypeLib\{3DB63102-0FED-11D0-9A44-0080C70FB085}\1.0\0\win32 = first.tlb
If from the Project menu you choose Project Info for the FIRST project, you'll see that there are three panes of information. The third is called Servers, from which you can view or change the information relating to each OLE Public in your project. Note that this information will only appear after you've built your project into an EXE or DLL, because only then is the OLEPUBLIC keyword meaningful.
The Instancing drop-down allows you to specify how an out-of-process server will behave. If this is set at "Single Use" and builds an EXE, then each OLE Automation client that instantiates this server will get its own private instance of the server. For example, if you type OX = CreateObject("first.myform ") and then OY = CreateObject("first.myform"), you'll actually get two running processes. (Note the SET EXCLUSIVE OFF line in the LOAD event; this is important!) You can use tools like Task Manager to see two first.exes running. This will also be the case if there were two different clients, say, Visual Basic and Visual FoxPro.
If you change the instancing on "Myform" to "Multiple Use" (which means this class can be used multiple times, or can have multiple "Masters"), then instantiate both OX and OY as above, TaskManager shows only a single Server instance serving multiple "Masters." You'll also note that moving the record pointer in one instance will change it in the other. You can change the DataSession property to 2—PRIVATE to avoid this.
The third option on Instancing is "Not Creatable." Why would you want this? Suppose you have a Class library with at least one OLE Public in it for one project. Now suppose you want to use some of the classes in that class library for another project, but you didn't want to use that OLE Public class. Using this instancing option means that the OLE Public will be ignored to solve this problem.
With just a few lines of code, you can take a FoxPro 2.x program and make it an OLE server. For example, the Laser sample that ships with FoxPro 2.6 is a laser disk library manager program. The main program is Laser.SPR. To turn this into an OLE object callable from Visual Basic or Excel, create a new project and add a new main program:
ox = create("laser")
define class laser as custom olepublic
proc init
cd d:\fpw26\sample\laser && change dir to the right place
set path to data
this.application.visible = .t. && make us visible.
proc doit
do laser.spr
enddefine
Add laser.spr and rebuild the project. You'll need to manually add a couple of Laser files. Now, you can try OX = Createobject("laser.laser"), and then OX.Doit. The laser application is now running as an OLE Automation server! If you modify the LASER.SPR program so that it doesn't close the LASER table when the READ (remember this command?) is finished, then you can query what laser disc title was chosen: ?ox.application.eval("title")
Your FIRST.PJX project contained a simple OLE server that just put up a message box. This is not very useful, especially since most OLE servers should be able to run on an unattended server machine. Let's modify this sample to generate HTML strings. Add to your FIRST.PRG the class definition for dbpub:
DEFINE CLASS dbpub AS custom OLEPUBLIC
cDataPath = "c:\vfp\samples\data"
&& change this to point to the dir where your TESTDATA.DBC is
cDir = ""
cSrvName = "first"
PROCEDURE INIT
CLOSE DATA ALL
SET TALK OFF
SET SAFETY OFF
SET EXCLUSIVE OFF
SET DELETED ON
DECLARE INTEGER GetPrivateProfileString in win32api String,String,String,;
String @, integer,string
if _vfp.startmode>1 && automation server
LOCAL buf,nlen
buf=space(400)
DECLARE INTEGER GetModuleFileName in win32api Integer,String @,Integer
* As an EXE or DLL, we need to find the home directory, not the curdir() and not
* the dir of the runtime. The classlibrary property
* shows this for PRGs, but not VCXs
IF _vfp.startmode = 3 && inproc dll
DECLARE INTEGER GetModuleHandle in win32api String
nlen=Getmodulefilename(GetModuleHandle(this.csrvname+".dll"),@buf,len(buf))
ELSE
nlen=Getmodulefilename(0,@buf,len(buf))
ENDIF
buf = LEFTC(buf,nlen)
this.cdir = LEFTC(buf,RATC('\',buf)-1)
this.csrvname = SUBSTRC(buf,RATC('\',buf)+1)
this.csrvname = LEFTC(this.csrvname,AT_C(".",this.csrvname)-1)
endif
IF !EMPTY(this.cDir)
SET DEFAULT TO (this.cDir)
ENDIF
SET PATH TO (this.cDatapath)
PROCEDURE dbpub(parms,inifile,relflag)
LOCAL rv
rv = "HTTP/1.0 200 OK"+CHR(13)+CHR(10)
rv = m.rv + "Content-Type: text/html"+CHR(13)+CHR(10)+CHR(13)+CHR(10)
rv = m.rv + "<HTML>"
rv = m.rv + "Parm1 =" + m.parms + "<p>"
rv = m.rv + "Parm2 =" + m.inifile
RETURN m.rv
ENDDEFINE
Note that the INIT method here is almost identical to the LOAD event for the form above. I've just removed the USE EMPLOYEE line. The DBPUB method takes three parameters and returns an HTML string. Test it out with ox=CreateObject('first.dbpub') then ?ox.dbpub("parm1","parm2",1234).
Now we need to find an Internet Web server. You can use Windows NT® 4.0 Server, which comes with Microsoft Internet Information Server (IIS), or NT3.51, Service Pack 4 with IIS. Windows 95 and Windows NT 4.0 Workstation will also work with Personal Web server. Other ISAPI compatible servers have been rumored to work. Take the file VFP\SAMPLES\SERVERSFOXISAPI\FOXISAPI.DLL (note that the latest copy of this can be found on the Microsoft Visual FoxPro Web site) and place it in the SCRIPTS directory of your IIS.
Suppose the Web site is named "myweb." Start up a Web browser, and type in the URL: http://myweb/scripts/foxisapi.dll. You should get a Foxisapi error page, indicating that the DLL is working correctly. Try adding arguments, such as http://myweb/scripts/foxisapi.dll/arg1.arg2.method?test. The first and second arguments are interpreted as the ProgID of an OLE Automation server. The third parameter is interpreted as the method name, and whatever is after the "?" is interpreted as a parameter to pass.
Before we run your FIRST server from the web, we need to tend to some security issues if you're running on Windows NT 4.0. IIS runs as a service on NT, which means it can run with no user logged in. It also has very limited rights to various parts of the operating system. Services normally do not have a desktop, so a MessageBox from a service won't even show up on the screen and will just hang the service.
Windows NT 3.51 will allow any service rights to read the registry and launch an OLE server. This means you have little control over somebody attaching to your machine and using it to run tasks. If you run the IIS service manager, and look at the WWW service properties, you'll notice that the anonymous user logs in as IUSR_MYWEB. From the Visual FoxPro command window, type !/n DCOMCNFG. This is an Windows NT 4.0 utility that you must run after registering an OLE server. Add "IUSR_MYWEB" to the three button dialogs on the DCOMCNFG Default Security page. Look for "myfirst" in the list of applications on the Applications Page. Choose Properties> Identity, and run as the Interactive User. Your particular machine/network setup might vary from these procedures.
Because Visual FoxPro registers the server each time after a build, you may have to run DCOMCNFG after each build. You can just run it and exit without changing anything. An alternative is to connect to the server machine from another machine and just copy the new server EXE or DLL over the old one (assuming it's already registered on the server machine).
Now that we've got security, lets hit our Web site with "myweb/scripts/foxisapi.dll/myfirst.dbpub.dbpub?test" The returned HTML string shows the parameters. The first parameter is just whatever is after the "?". The second parameter is the name of a .INI file that contains information about the Web hit. You can see this information get parsed out if you do the following:
MODIFY CLASS isform OF HOME()+"samples\servers\foxisapi\isapi" METHOD genhtml
Here's a sample .INI file:
The INI file looks like this:
[FOXISAPI]
Request Method=GET
Query String=
Logical Path=/foxis.employee.startup
Physical Path=d:\inetsrv\wwwroot\foxis.employee.startup
FoxISAPI Version=FoxISAPI v1.0
Request Protocol=HTTP/1.0
Referer=/scripts/foxisapi.dll
Server Software=Microsoft-Internet-Information-Server/1.0
Server Name=127.0.0.1
Server Port=80
Remote Host=127.0.0.1
Remote Address=127.0.0.1
[All_HTTP]
HTTP_ACCEPT=*/*, q=0.300,audio/x-aiff,audio/basic,image/jpeg,image/gif,text/plain,text/html
HTTP_USER_AGENT=Mozilla/1.22 (compatible; MSIE 1.5; Windows NT)
HTTP_CONNECTION=Keep-Alive
HTTP_EXTENSION=Security/Digest
[Accept]
*/*=Yes
q=0.300=Yes
audio/x-aiff=Yes
audio/basic=Yes
image/jpeg=Yes
image/gif=Yes
text/plain=Yes
text/html=Yes
[SYSTEM]
GMT Offset=-28800
The third parameter is just a number passed in by reference. It's actually meaningless to the server. Each time a Web hit occurs, foxisapi.dll will instantiate the ProgID, invoke the method passing the parms, return the generated HTML to the Web site, and then release the OLE server. It's pretty inefficient to start and stop the OLE server each time. However, if the server changes the value to 0, then the server won't be released and will be already running and ready for the next Web hit. To release the server, it need only not alter the value.
To release all Visual FoxPro servers on a single server, you can use the URL: "/scripts/foxisapi.dll/reset". This will call all cached existing instances of Visual FoxPro servers and tell them to release.
Now that we know how to have Visual FoxPro OLE servers generate HTML, let's get a little fancier and publish a .dbf on the Web. Change the DBPUB method to the code below:
PROCEDURE dbpub(parms,inifile,relflag)
LOCAL rv,m.ii,mt
rv = "HTTP/1.0 200 OK"+CHR(13)+CHR(10)
rv = m.rv + "Content-Type: text/html"+CHR(13)+CHR(10)+CHR(13)+CHR(10)
rv = m.rv + "<HTML>" + '<table border = "10">' + chr(13)
USE (m.parms) SHARED && Open the table
for ii = 1 to FCOUNT()
IF type("EVAL(FIELD(m.ii))") = 'G'
loop && don't proc general fields
ENDIF
rv = rv + "<th>" + PROPER(field(m.ii)) + "</th>" + chr(13)
endfor
rv = rv + "</tr>" + chr(13)
SCAN
rv = rv + "<tr>"
for ii = 1 TO FCOUNT()
IF type("EVAL(FIELD(m.ii))") = 'G'
loop && don't proc general fields
ENDIF
mt = eval(field(m.ii))
do case
case type("mt") = 'C'
rv = rv + "<td>" + mt + "</td>"
case type("mt") = 'T'
rv = rv + "<td>" + TTOC(mt) + "</td>"
case type("mt") $ 'NY'
rv = rv + "<td align=right>" + str(mt,8) + "</td>"
case type("mt") = 'D'
rv = rv + "<td>" + DTOC(mt) + "</td>"
endcase
rv = rv + chr(13)
endfor
rv = rv + "</tr>"
ENDSCAN
rv = rv +"</table>"
return rv
The first parameter specifies the name of the table. All this does is loop through all the columns and rows in a DBF and returns an HTML Table string. Try it with a few URLs: "HTTP://myweb/scripts/foxisapi.dll/first.dbpub.dbpub?customer", "HTTP://myweb/scripts/foxisapi.dll/first.dbpub.dbpub?employee".
By just putting the name of a table in a URL, it is published on the Web. Pretty powerful stuff!
In your Visual FoxPro SAMPLES\SERVERS directory is a directory called FOXISAPI. This is a sample form class that just allows the user to edit/view the EMPLOYEE data in TESTDATA.DBC. However, the same form can be deployed in four different ways: as a normal Visual FoxPro form, as a Visual FoxPro run time, as an OLE Automation server from an OLE Automation client, and over the Internet using any Web browser!
This sample requires only that end users have a machine capable of running any Internet browser, which means it will allow you to deploy Visual FoxPro applications on 286s, Macs, Unix boxes, and maybe even personal digital assistants! Any changes made to the application only need to be done once. The changes will be automatically propagated to the other deployment platforms.
The readme.txt file in that directory explains how to install and configure the sample. For the latest version of this sample, go to the Visual FoxPro home page at http://www.microsoft.com/vfoxpro/. Also be sure to read the comments in foxisapi.cpp and isapi.vcx for tips.
The FOXISAPI sample takes advantage of the third parameter to do smart instancing of the server. The CMD method allows the user to execute DOS commands or evaluate FoxPro expressions on the server. If RESET is passed as a DOS command, the server will not change the third parameter to 0, and thus the server instance gets released.
Most of the work in the FOXISAPI sample is done by the GENHTML method. The result is a two column HTML table with labels for the field names and text box controls for the field values. It does an AMEMBERS( ) function call to determine dynamically what objects are on the form and to place the objects into an array. It then does a two-dimensional bubble sort to sort the objects into x,y order. HTML is stream based, whereas Visual FoxPro is pixel based.
The method then loops through each form member in the array elements and tries to generate HTML for them. If the object has its own GENHTML method, then it is invoked to return an HTML string. The class of the object is examined to determine what kind of object it is, and the appropriate HTML is generated. If there's a text box, then an associated label is searched for. Both of these get entered into the HTML table for the form.
After the form is generated, GENHTML then looks at the .INI file and appends the data to a table and the returned HTML.
Web servers can be hit multiple times by multiple clients. If client A clicks NEXT and client B clicks NEXT, it's important that the server knows which record was current for each client. This is handled with a cookie, which is just a unique ID assigned to the client. Subsequently served Web pages to that client include the same cookie hardcoded in its HTML.
Error Handling is extremely important from an OLE server, especially one that serves up Web pages. The server is typically unattended, and often doesn't even have a desktop to which error dialogs can be displayed. If there's an error in the FOXISAPI sample, the error method generates an Error HTML page and the GENHTML method returns that error page.
The LOG method in the FOXISAPI sample is useful: it just adds a line to a text file. The file can be examined to see what the server is doing. Use this liberally when debugging your applications. Microsoft Visual Studio™, which comes with Visual C++, has an option to automatically reload externally modified files, so you can just watch the log as the file changes.
Some parameters are passed via URLs, and thus some characters are not legal. For example, the chars "<>()" are not legal. They are converted by the Web browser to their hexadecimal equivalents prepended by a "%". The FIXURL method reverses the conversion.
When your Web server is hit from a Web site, how do you know who hit you? There are three new functions added to Foxtools that will help determine this. It also depends on whether you're on an intranet or the Internet. These functions call the Win Sockets API to resolve a remote IP Address to a host name.
I've added three functions to Foxtools that will resolve IP addresses.
{"_WSOCKSTARTUP", (FPFI) WSockStartup, 7, "I,R,R,R,R,R,R"},
{"_WSOCKCLEANUP", (FPFI) WSockCleanup, 0, ""},
{"_WSOCKGETHOSTBYADDR", (FPFI) WSockGetHostByAddr, 2, "C,R"},
Call _WSOCKSTARTUP and _WSOCKCLEANUP to initialize/uninitialize the Winsock library. The _WSOCKGETHOSTBYADDR function takes an IP address string like "123.123.123.123" and resolves it into the name of the machine.
// store 0 to buff1,buff2,buff3,buff4,buff5,buff6
//?_wsockstartup(256 * 1 + 1,@buff1,@buff2,@buff3,@buff4,@buff5,@buff6)
// integer version, wVersion,wHighVersion,szDescription,szSystemStatus,iMaxSockets,iMaxUdpDg
//? _wsockgethostbyaddr("123.123.123.123",@buff1) &&returns 1 on success, 0 on failure
// ?buff1 && returns the host name
// Note: if the target machine is not available, this call can take a long time.
Using Internet Information Server 3.0 with Active Server Framework (with the code name Denali) you can have your Web server send Active Server Pages (ASP). These ASP files are HTML files with special tags enclosed in the delimiters "<%" and "%>". These tags delimit both client-side and server-side scripts. Those that are server side will be executed on the server and will not show up on the client side (when the client Internet browser does a View>Source, for example).
The server-side scripting can be in any scripting language, but defaults to VBScript. You can create an ASP file that has a few lines of script that will return HTML for your ISAPI form:
<% set ox = server.CreateObject("foxis.employee")%>
<% = ox.Startup()%>
Here, a server script variable called ox is assigned an object reference to the Visual FoxPro automation server "foxis.employee". The second line invokes the startup method on the object, and returns the results as HTML.
In this case, the OLE server instancing model is handled by Denali. Because ox is a script variable, its lifetime is the lifetime of the script. That means the entire Visual FoxPro server instance is instantiated and released for each Web hit. Denali provides variables scoped to a session to allow the OLE server to have a longer lifetime:
<% set session("ox") = server.CreateObject("foxis.employee")%>
<% = session("ox").Startup()%>
Instead of using a script variable, we're using a session variable called ox.
Using this method of starting the OLE server, the server instance will exist until the session.timeout (default 20 minutes) expires.
The ISFORM class of ISAPI has a property called fASP which is either True or False, indicating whether or not the class was instantiated from an ASP page or from an HTML page (using the HREF /scripts/foxisapi.dll/foxis.employee.startup). If this flag is true, then the server can return generated HTML without an HTML Content header (there already is one in the ASP page. Also, an ASP page generates a cookie that can be used as the unique identifier for the session, and this is parsed out in the STARTUP method.
<% set session("ox") = server.CreateObject("foxis.employee")%>
<% = session("ox").Startup("ASP Cookie=" + Request.ServerVariable("http_cookie"),"",0)%>
OLE was designed so that an out-of-process automation server does not have to live on the same machine as the client. It can be run on any other machine on the network. This remote automation capability means that OLE server applications don't care whether they're to be deployed on a local machine or a remote one. Also, because a single server can serve multiple clients, new classes of client/server application architectures are possible.
OLE servers allow application developers to deploy their applications as OLE clients or OLE servers, or both. That is, an application can have two or more components: one that acts as an OLE client, and one that acts as an OLE server. For example, a customer form might be a simple OLE client application running on dozens of machines, but it might talk to a single OLE server application that lives on a server machine somewhere that might enforce business rules, such as no new orders from a customer who has outstanding debts. The business rule enforcer might then talk to the actual data itself, which might be on Microsoft SQL Server or Visual FoxPro tables on yet another machine. If the customer form were to change, then only the front end needs to be updated. If the business rules change, then only the single business rule enforcer needs to be replaced. This "three-tier" client/server architecture also means that data processing loads get distributed to specialized applications/machines designed and optimized to do the job.
Another useful architecture is to have a client application spin off computationally- or resource-intensive tasks to other machines, so the client machine is free for other tasks such as printing, database updating, report generation, end-of-month processing, and so on.