Allowing Optional parameters in your COM objects
It’s simple to create a VFP object that can be used within other applications. I show how useful it is in Blogs get 300 hits per hour: Visual FoxPro can count.
That sample builds the T1.C1 object that uses methods with multiple parameters. T1.C1 is a general purpose server which in that example is shown to serves up web pages on a web server. It can be used for running any VFP code. Because the code to run is not built into the object itself, modifying the web server functionality doesn’t require shutting down and restarting the web site or web application as most other applications do.
When calling Foxpro methods and functions, all parameters are optional by default. In other words, if the function expects 3 parameters, you can still pass 0 arguments, and the unpassed parameters will default to False(.f.)
When building a COM object, Foxpro generates a TypeLibrary describing all the methods and their parameters. When calling that object via late binding, the additional required parameters default to .f.
It’s simple to modify the generated TypeLib to indicate some parameters are optional: we just add a little code to the project hook posted in Strongly typed methods and properties
The sample below adds to the same project hook to massage the generated TypeLibrary to indicate optional parameters.
If the helpstring has a “|” character, the following text is interpreted as a TypeLibrary directive.
- If it starts with “%”, then the value is interpreted as an integer (like 2), indicating that parameters starting with the 2nd are optional.
- Else it is interpreted as a strongly typed object to return (see Strongly typed methods and properties)
Run the code below, then start Excel or VB.Net
Excel
In Excel, choose Tools->Macros->VB Editor. For the menu option to open VB Editor in Excel 2007, use the Developer Tab on your ribbon. If it’s not there, choose Office Button, Excel Options, Show Developer Tab in Ribbon. Alt-F11 works in both versions of Excel. To use early binding, add a reference to T1.DLL by choosing Tools->References and add a checkmark to “T1 Type Library”
Then Insert->Module and paste this code
Sub foo()
Dim xEarly As New t1.c1 ' for early binding
MsgBox (xEarly.MyEval("iif(sys(2334)='1','Early bound call ','Late bound call ') + version(1)"))
Set xLate = CreateObject("t1.c1") ' for late binding
MsgBox (xLate.MyEval("iif(sys(2334)='1','Early bound call ','Late bound call ') + version(1)"))
End Sub
Now hit F5 to run the code
The code uses SYS(2334) to indicate whether or not it was an early or late bound call.
Because the Optional parameters are specified, the line is simpler than
MsgBox (x.MyEval("version(1)", 0, 0, 0, 0))
Also, with early binding Excel’s intellisense indicates optional parameters with square brackets.
To use late binding, don’t add the reference, replace the Dim line with this: Set x = CreateObject(“t1.c1”)
VB.Net
In Visual Studio, choose File->New->Project->VB->Windows Form Application. Again, add a reference to get early binding (Late binding works as in Excel.) Project->Add Reference->Browse and navigate to the T1.DLL you just built.
Dbl click the form to add this code to the Load method:
Dim xEarly As New t1.c1 ' for early binding
Dim cCmd As String = "iif(sys(2334)='1','Early bound call ','Late bound call ') + version(1)"
MsgBox(xEarly.MyEval(cCmd))
Dim xLate = CreateObject("t1.c1") ' for late binding
MsgBox(xLate.MyEval(cCmd))
'This one uses reflection to make the early one invoke latebound
MsgBox(xEarly.GetType().InvokeMember("MyEval", Reflection.BindingFlags.InvokeMethod, _
Nothing, xEarly, New Object() {cCmd}))
Me.Close() ' close the form
In C#:
t1.c1 xEarly = new t1.c1Class();
string cCmd = "iif(sys(2334)='1','Early bound call ','Late bound call ') + version(1)";
System.Windows.Forms.MessageBox.Show(xEarly.MyEval(cCmd, 0, 0, 0, 0).ToString());
string cLate = xEarly.GetType().InvokeMember("MyEval",System.Reflection.BindingFlags.InvokeMethod,
null,xEarly,new object[] {cCmd}).ToString();
System.Windows.Forms.MessageBox.Show(cLate);
this.Close(); // close the form
Does C# support late binding? No, except through reflection.see Programming Office Applications Using Visual C#
Does C# support optional params? (ILDASM of Inerop.T1.Dll looks the same as for VB.) No: see Understanding Optional Parameters in COM Interop
See also
Binding for Office automation servers with Visual C# .NET
Programming with Visual Basic Versus C#
Use temporary projects in Visual Studio
Blogs get 300 hits per hour: Visual FoxPro can count.
A Visual Basic COM object is simple to create, call and debug from Excel
CLEAR
SET SAFETY OFF
*!* Use a Project hook to make general purpose COM server with optional parameters
*!* Run this code in Excel
*!* Sub foo()
*!* Dim x As New t1.c1 ' for early binding
*!* MsgBox (x.MyEval("iif(sys(2334)='1','Early bound call ','Late bound call ') + version(1)"))
*!* Set x2 = CreateObject("t1.c1") ' for late binding
*!* MsgBox (x2.MyEval("iif(sys(2334)='1','Early bound call ','Late bound call ') + version(1)"))
*!* End Sub
*!* in VB.Net:
*!* Dim xEarly As New t1.c1 ' for early binding
*!* Dim cCmd As String = "iif(sys(2334)='1','Early bound call ','Late bound call ') + version(1)"
*!* MsgBox(xEarly.MyEval(cCmd))
*!* Dim xLate = CreateObject("t1.c1") ' for late binding
*!* MsgBox(xLate.MyEval(cCmd))
*!* 'This one uses reflection to make the early one invoke latebound
*!* MsgBox(xEarly.GetType().InvokeMember("MyEval", Reflection.BindingFlags.InvokeMethod, _
*!* Nothing, xEarly, New Object() {cCmd}))
*!* Me.Close() ' close the form
#if .t.
TEXT TO mystr NOSHOW
DEFINE CLASS c1 as session olepublic
proc MyDoCmd(cCmd as string,p2 as Variant,p3 as Variant,p4 as Variant,p5 as Variant) ;
helpstring 'Execute a command|%2'
&cCmd
proc MyEval(cExpr as string,p2 as Variant,p3 as Variant,p4 as Variant,p5 as Variant) ;
helpstring 'Evaluate an expression|%2'
RETURN &cExpr
ENDDEFINE
ENDTEXT
STRTOFILE(mystr,"c1.prg")
IF !FILE("t1.pjx") && only rebuild if doesn't exist so we don't regen guids and pollute registry
BUILD PROJECT t1 FROM c1
ENDIF
MODIFY PROJECT t1 nowait
_vfp.ActiveProject.ProjectHook = NEWOBJECT('myphook') && use projecthook to modify typelibrary if necessary
nerror=0
TRY
BUILD MTDLL t1 FROM t1
CATCH TO oex
?oex.message
nerror=1
FINALLY
_vfp.ActiveProject.Close
ENDTRY
IF nerror>0
RETURN
ENDIF
*retu
#endif
LOCAL x as t1.c1
x=CREATEOBJECTEX("t1.c1","","")
?x.MyEval("_vfp.servername")
?x.MyEval("iif(sys(2334)='1','Early bound call ','Late bound call ') + version(1)")
DEFINE CLASS MyPHook AS ProjectHook
PROCEDURE GetType(oType as tli.VarTypeInfo) as String
LOCAL cstr,nType
nType=oType.VarType
cstr=""
IF oType.PointerLevel>0 AND !ISNULL(oType.TypeInfo)
cstr=cstr+oType.TypeInfo.Name+" *"
RETURN cstr
ENDIF
IF BITAND(nType,8192)>0
cstr="VT_ARRAY | "
nType=nType-8192
ENDIF
DO case
CASE nType=0
cstr=cstr+ "VT_EMPTY"
CASE nType=2
cstr=cstr+ "VT_I2"
CASE nType=3
cstr=cstr+ "LONG"
CASE nType=7
cstr=cstr+ "DATE"
CASE nType=8
cstr=cstr+ "BSTR"
CASE nType=9
cstr=cstr+ "VT_DISPATCH"
CASE nType=11
cstr=cstr+ "BOOL"
CASE nType=12
cstr=cstr+ "VARIANT"
CASE nType=13
cstr=cstr+ "VT_UNKNOWN"
CASE nType=16
cstr=cstr+ "VT_I1"
CASE nType=17
cstr=cstr+ "VT_UI1"
CASE nType=18
cstr=cstr+ "VT_UI2"
CASE nType=19
cstr=cstr+ "VT_UI4"
CASE nType=22
cstr=cstr+ "VT_INT"
CASE nType=23
cstr=cstr+ "VT_UINT"
CASE nType=24
cstr=cstr+ "VOID"
CASE nType=25
cstr=cstr+ "VT_HRESULT"
OTHERWISE
SET STEP ON
ENDCASE
RETURN cstr
PROCEDURE FixTLB(DllName as String)
fModified=.f.
DIMENSION asec[2] && preserve 2 sections of EXE
h=FOPEN(DllName)
fpos=FSEEK(h,0,2) && go to EOF
FOR i = 1 TO 2
FSEEK(h,fpos-14,0)
pmt=FREAD(h,14)
sz=CTOBIN(substr(pmt,11,4),"4sr")
FSEEK(h,fpos-sz,0)
asec[i]=FREAD(h,sz)
fpos = fpos - sz
ENDFOR
FCLOSE(h)
LOCAL otlb as "tli.tliapplication"
LOCAL otli as TLI.TypeLibInfo
otlb=NEWOBJECT("tli.tliapplication")
otli=otlb.TypeLibInfoFromFile(DllName)
SET TEXTMERGE TO t.idl ON noshow
\//Generated .IDL FILE(by Visual Foxpro tlibtest by Calvin Hsia)
\//
\// typelib filename tlibtest.dll, generated <<DATETIME()>>
\[
\ uuid(<<CHRTRAN(otli.GUID,"{}","")>>),
\ version(1.0),
\ helpstring("<<otli.HelpString>>")
\]
\library <<otli.Name>>
\{
\ importlib("stdole2.tlb");
\
\ // Forward declare types defined in this typelib
FOR EACH oCC as tli.CoClassInfo IN otli.CoClasses
FOR EACH oInt as TLI.InterfaceInfo IN oCC.Interfaces
\ interface <<oInt.Name>>;
ENDFOR
ENDFOR
FOR EACH oCC as tli.CoClassInfo IN otli.CoClasses
FOR EACH oInt as TLI.InterfaceInfo IN oCC.Interfaces
\ [
\ odl,
\ uuid(<<CHRTRAN(oInt.GUID,"{}","")>>),
\ helpstring("<<oInt.HelpString>>"),
\ hidden,
\ dual,
\ nonextensible,
\ oleautomation
\ ]
\ interface <<oInt.Name>> : <<oInt.ImpliedInterfaces.Item(1).Name>> {
FOR EACH oMem as TLI.MemberInfo IN oInt.Members
IF omem.MemberId < 0x6000000 && not the IDispatch/IUnknown
cHelpstring=oMem.HelpString
nOptional=1000
cRetType=this.GetType(oMem.ReturnType)
IF ""!=cHelpstring
cRest=cHelpstring
n=AT("|",cHelpstring)
cHelpString=LEFT(cHelpstring,n-1) && the real helpstring. Now process directives
*Directives: start with "|"
IF RIGHT(cRest,1) != "|" && add terminator if needed
cRest=cRest+"|"
ENDIF
DO WHILE n>1
fModified=.t.
cRest=SUBSTR(cRest,n+1)
n=AT("|",cRest) && see if there are any more directives
IF n>0
cTemp=LEFT(cRest,n-1)
cTemp=SUBSTR(cTemp,1) && remove the delimiters
* ?"===",cRest,n,cTemp
DO CASE
CASE LEFT(cRest,1)="%"
nOptional=VAL(SUBSTR(cTemp,2))
OTHERWISE
cRetType=cTemp
ENDCASE
ENDIF
ENDDO
ENDIF
\ [id(<<TRANSFORM(omem.MemberId,"@0x")>>)
IF oMem.InvokeKind>1
\\,<<IIF(oMem.InvokeKind==2,"propget", "propput")>>
ENDIF
IF ""!=cHelpstring
\\,helpstring("<<cHelpString>>")
ENDIF
\\]
\ HRESULT <<oMem.Name>>(
IF INLIST(oMem.InvokeKind,2,4)
IF oMem.InvokeKind=2
\\[out, retval] <<cRetType>>* <<oMem.Name>>
ELSE
\\[in] <<cRetType>> <<oMem.Name>>
ENDIF
\\);
ELSE
fHasAttr = .f.
nParmCount=0
FOR EACH oParm as tli.ParameterInfo IN omem.Parameters
nParmCount=nParmCount+1
cAttr=""
IF BITAND(oParm.Flags,1)>0
cAttr=cAttr+", in"
ENDIF
IF nParmCount >= nOptional
cAttr=cAttr+", optional"
ENDIF
IF BITAND(oParm.Flags,2)>0
cAttr=cAttr+", out"
ENDIF
IF BITAND(oParm.Flags,8)>0
cAttr=cAttr+", retval"
ENDIF
IF ""!=cAttr
\\[<<SUBSTR(cAttr,3)>>]
ENDIF
\\ <<this.gettype(oParm.VarTypeInfo)>> <<oParm.Name>>
IF omem.Parameters.Count>0
\\,
ENDIF
ENDFOR
\\[out, retval] <<cRetType>>* RetVal
\\);
ENDIF
ENDIF
ENDFOR
\ };
ENDFOR
ENDFOR
\
FOR EACH oCC as tli.CoClassInfo IN otli.CoClasses
\ [
\ uuid(<<CHRTRAN(occ.GUID,"{}","")>>),
\ helpstring("<<occ.HelpString>>")
\ ]
\ coclass <<occ.Name>> {
FOR EACH oInt as TLI.InterfaceInfo IN oCC.Interfaces
\ [default] interface <<oInt.Name>>;
ENDFOR
\ };
ENDFOR
\};
SET TEXTMERGE to
otlb=0 && release, so we can insert new typelib into it
otli=0
IF fModified
cVars = "c:\Program Files\Microsoft Visual Studio 9.0\Vc\bin\vcvars32.bat"
IF !FILE(cVars)
cVars=LOCFILE("c:\Program Files\Microsoft Visual Studio 8\Vc\bin\vcvars32.bat")
* cVars=LOCFILE("c:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\bin\vcvars32.bat")
ENDIF
IF !FILE(cVars)
?"Aborting: can't find VS"
RETURN
ENDIF
TEXT TO mybat textmerge
call "<<cVars>>"
midl t.idl
ENDTEXT
STRTOFILE(mybat,"t.bat")
!cmd /c t.bat > t.txt
?FILETOSTR("t.txt")
?"done midl"
DECLARE integer BeginUpdateResource IN WIN32API string , integer
DECLARE integer EndUpdateResource IN WIN32API integer, integer
DECLARE integer UpdateResource IN WIN32API integer,string,integer,integer, string, integer
DECLARE Integer GetLastError IN win32api
h=BeginUpdateResource(DllName,0)
strTlb=FILETOSTR("t.tlb")
UpdateResource(h,"TYPELIB",1,0x409,0,0)
UpdateResource(h,"TYPELIB",1,0x409,strTlb,LEN(strTlb))
IF EndUpdateResource(h,0)=0
?"Err=",GetLastError()
ENDIF
h=FOPEN(DllName,2)
fpos=FSEEK(h,0,2)
FOR i = 1 TO 2
FWRITE(h,asec[i])
ENDFOR
FCLOSE(h)
?"TypeLib Modification Done"
ELSE
?"TypeLib Modification unnecessary"
ENDIF
PROCEDURE AfterBuild(nError)
IF nError=0
this.FixTLB(JUSTSTEM(_vfp.ActiveProject.Name)+".dll")
ENDIF
ENDDEFINE
End of code
Comments
Anonymous
May 17, 2007
It appears that the GetType in the VB.Net sample above does not work in VS 2005: it works in the next version, which I was using to test. To make reflection work in VS 2005, create a temporary variable: Dim oTemp as Object = xEarly Then use oTemp.GetType(), rather than xEarly.GetType()Anonymous
May 21, 2007
The comment has been removedAnonymous
July 23, 2007
Can't you also accomplish this with _COMATTRIB? From the VFP documentation, the 5th element of _COMATTRIB represents "Number of parameters - If you specify fewer than the actual number of parameters, the number greater than those declared are optional." Is there some benefit to modifying the type library directly instead of using the built in VFP feature?