Level Extreme platform
Subscription
Corporate profile
Products & Services
Support
Legal
Français
Articles
Search: 

How to rebuild from Class Browser's export
Dragan Nedeljkovich, November 1, 2006
In this article, Dragan Nedeljkovich talks about how to rebuild from Class Browser's export functionality. Ever since Visual FoxPro 3, we have had the object browser. It has this nice feature to export a class or a form as code. Five versions later, it is still there. But, it still produces out code...
Summary
In this article, Dragan Nedeljkovich talks about how to rebuild from Class Browser's export functionality. Ever since Visual FoxPro 3, we have had the object browser. It has this nice feature to export a class or a form as code. Five versions later, it is still there. But, it still produces out code we can't really use. In this article, Dragan provides a way to extand this functionality.
Description
Ever since Visual FoxPro 3, we have had the object browser. It has this nice feature to export a class or a form as code. Five versions later, it is still there. But, it still produces out code we can't really use.

Why can't we use that code? Couldn't it just be run as is? In some simpler cases, yes, that could apply. If you export a form with three buttons and a textbox, it will produce executable code. But, as soon as you have a custom property with a blank value, which displays as "(none)" in the Properties and Methods sheet (PEM), it generates a property assignment with no value at all, which won't even compile.

But, it gets worst. Until VFP8, it didn't properly qualify methods. Let's suppose you had a pageframe with a commandbutton on each page: form1.pageframe1.page1.command1 and form1.pageframe1.page2.command1. If both of them had a click method, the exported code would say “command1.click” for both of them, without the full path to the object. This was fixed in VFP9. So now, it creates code like this:

Procedure Form.Pageframe1.pgNew.grdV_evprop.Column1.Text1.DblClick
This helps a lot. But, this code also can't be run. The form name shouldn't be there. It should be just Pageframe1.pgNew... etc. Likewise, statements like:
ADD OBJECT form.pageframe1.pgnew.obizevent AS bizevent
won't compile anyway. The add object command can only accept a single name, not the full hierarchy. So, why bother with code which can't be run? Because it can. We cannot use it by just issuing a command such as DO ViewCode, but it can be used with a relatively simple builder (BLDFROMCB.PRG, in attachment to this article) to reconstruct the original class or form. It is not that we can't live without such a builder, but as Jerry Pournelle would say, when you need it, you need it bad. There are so many solutions to our problems posted in the form of Class Browser exported code, which we had to copy and paste into real executable code.

This builder is not a replacement for Prg2Vcx. This just builds a scx or vcx from Class Browser's export.

The builder

To get this builder to work, I needed access to the classlibs for the controls on the form. This requires one more tweak to the Class Browser's code, so the - already syntactically wrong - Add Object statement will become even more wrong and more useful. The tweak is to add "of {classlib}" to the exported statement. This and the other optional tweaks are listed in the appendix of this article.

Of course, the class libraries, a prg or a vcx, it doesn't matter, need to exist on the drive while I run the builder. They also need to be somewhere along the path for the builder to find them. You may need to edit these paths in the viewcode.prg to match the paths on your disk. If your export comes from an unpatched Class Browser, you would need the class libraries open (Set Classlib To ...) before you run the builder, or else it won't be able to create the objects which require them. If the exported code uses just base classes, this would not be an issue.

When bldFromCB.prg is run, it looks for viewcode.prg or any prg file you provide. It then splits it into lines (using filetostr() and aLines(), of course) and then parses it. First, it looks for an #include line. Since there is no programmable interface (short of hacking the .vcx or .scx) to do that from a builder once the editing starts, I had to resort to a dirty trick: a method called .GetIncludeFile sets the _include system variable to (the relative path to) the #include file mentioned in the exported code, if any.

Start: Define Class

There is a method called .LookFor() which just returns the index of the row where the search string was found. The line with Define Class is then located and chopped into pieces, the class name and parent class name, and the optional classlib, if that tweak is also applied to the Class Browser. It then creates a class with that name. A dialogue then appears where you need to tell where to save the file. It's the standard VFP dialog that you get on Create Class command. You can even rename the class if you want. The builder maintains a reference to the class being edited.

Next, it looks for the first AddObject line – which is the first after the properties. It scans the lines between the Define Class and AddObject and reads the properties from them. Same code is later used to set the properties of added objects. The code doesn't care whether the property is added or it exists in the parent class: .AddProperty() works in both cases.

Now is the time to add the objects. First, the builder needs to know where the last object is in the code, so it looks for the first line beginning with Procedure. Then it goes one object at a time, analyzing the lines one by one. The first and most complicated step is adding the actual object. Fortunately, the exported code never jumps the gun; it never adds an object before its parent was added, and with the fully qualified names of objects to add, it is actually easy to extract the object name, its parent reference, class and classlib from the first line of an Add Object block, create the object (actually, call its parent's .newobject() method – yes, you can do that with objects in edit mode). The rest of the lines to analyze contain the properties of the newly added object.

There's a weird bit to these properties, if the object is composite. The properties of its members are listed right away – grid's column names, controlsources etc, pageframe's pages' captions etc. Of course, the count of such objects is set first, so the pageframe will have five pages before page5 is mentioned. However, with composite objects based on user defined classes, the exported code just lists their properties – sometimes even for those which aren't there anymore.

How can this happen? Assume you had a container with three textboxes and later decided you need only two textboxes. For some reason, the properties for the third textbox are still listed in your form, unless you edited it meanwhile. Note that they may exist as well in your exported code. For this reason, the builder checks for the existence of any member object before trying to get a reference to it and assign it a property.

Let's take a look at the code. All the code in the exported prg file is between the first Procedure statement and the EndDefine line. The fully qualified object name in each of the methods, no matter how wrong syntactically, now helps locate the object to which it belongs. The object name is parsed and a stack of object references to its parent and further ancestry is maintained, starting form the builder's this.oForm, which is a reference to the class being edited. Then the rest of the procedure is applied via .WriteMethod() to the proper object. Note that we again don't care whether the method is new to the object or it exists in its parent class: the third parameter of .WriteMethod() is always set to .t. (add if new) - it works the same for existing methods as it does for new ones.

After that, the builder just exits, and you have your form or whichever class it was ready in the editor. You only need to click save. Or you can close it without saving, and repeat the process while watching it in the debugger. It may just be slow enough to show an animated building of a form. Without the debugger, it's just too fast – and even with the debugger, my most complicated form with 196 records in the .scx file rebuilds in just a few seconds.

But, we're not finished yet.

Dataenvironment

There's one more tweak to the code of the Class Browser to be done. It can generate the code for the Dataenvironment. We may use it if we want to build a form from the exported code – if the original form had a dataenvironment, then our rebuilt form can also have it.

Two problems arise here: first, how do we know whether we have a form at all, and how to save it with the dataenvironment. I first tried to temporarily instantiate the object, so to check its .baseclass, but encountered problems with objects which just won't instantiate without their framework's objects. Instead of that, I just decided to have a parameter (anything non-empty would do) to tell the builder to create a .scx and not a class. If you pass a parameter and the exported code doesn't describe a form – well, it'll error out and do nothing.

The saving problem is more serious. You can't just add a DE object to a form. Actually you can, but it won't behave as expected – its code won't run in the usual sequence (i.e. before any of your code in the form and before any other controls are instantiated), but in the instantiation order instead. Since we have already added all other controls to the form, it would run after all of them are instantiated already. The only way around this is to pass a reference to the DE in the .SaveAs() method of the form we are editing. The builder will ask for a filename to save to, and save your scx under that name. This way, the Dataenvironment occupies 2nd and subsequent records in the scx, just as it should.

The code which reads the exported DE code is pretty much a repetition of the code we already used, it just passes a reference to the DE where needed.

The Class Browser code used to export the DE, for some reason, also exports the .left, .top, .height and .width for the DE and each object in it, which will generate ignorable errors.

Note that the form you are editing still does not have a DE – the saved one does. At this point you should just close it without saving, and open the one you just saved. That one should have the DE, and if its tables are at hand, you may even run it. Note also that the builder won't save the form in other cases (class or a scx without a DE), and this can be confusing a bit. But then, this is not a tool you use every day and does not have any GUI at all. It's just a prg that you DO.

About error handling

The error handling in the builder is rudimentary: the typically ignorable errors are ignored, and the rest are just echoed to _screen. Typical errors are caused by properties which are unwritable (hidden, read-only at design time), or objects which cannot be created (already exist, class library not open or not found). In case of an error you may just decide to see how serious is it, and whether you may want to finish manually what the builder couldn't, or may want to help it by opening one more classlib, adding a directory to your path or just choose to ignore them. Since the errors are on _screen, your editing window is probably hiding them. The old Fox trick is to press ctrl+shift+delete, which will hide all the windows, and you can see the _screen, read the errors and decide whether to save the result or not. If you choose to help the builder, simply don't save, do whatever you think may help, and run it again.

Appendix

The proposed tweaks to the Class Browser are optional - the builder will work regardless of the version which exported the code. It will just work better with them - for one, you won't have to have the classlibs open, and it may also add the dataenvironment.

To tweak the Class Browser, you need to have the xSource.zip, which is somewhere on your installation CD, and to unzip it. I assume the path to the browser.pjx is HOME()+"TOOLS\XSOURCE\VFPSOURCE\BROWSER” - and we only need to apply a few changes to Browser.prg as follows:

Somewhere around line 6120, after:

lcCode=lcCode+Iif(llHTML,[],[])+;
 Iif(Empty(lcClassLoc),""," ("+lcClassLoc+")")+CR_LF+ ;
 lcComment+"BaseClass:    "+Iif(llHTML,[],[])+ ;
 lcBaseClass+Iif(llHTML,[	],[])+CR_LF
add this to have the classlib in the Define Class line:
IF NOT EMPTY(lcClassLoc)
   lcParentClass = lcParentClass + " of "+CHR(34)+lcClassLoc+CHR(34)+" "
ENDIF
Below line 6270, after:
Do Case
   Case Not llHTML
      lcAddObject=lcAddObject+lcParentClass
add this to have the class location in Add Object:
If Not Empty(lcClassLoc) And Not toBrowser.cFileName==lcClassLoc
   lcAddObject = lcAddObject + [ of "]+lcClassLoc+["]
Endif
Finally, below line 6360, after:
lcCode=Strtran(Strtran(lcCode,MARKER,""),CR_LF+Tab+CR_LF)
Do While Left(lcCode,1)==Tab
   lcCode=Alltrim(Substr(lcCode,2))
Enddo
add this:
If llSCXMode
   Go toBrowser.aClassList[1,2]+1
   If Class="dataenvironment"
      lcCode=lcCode+CR_LF+CR_LF+brwDeCode(toBrowser)
   Endif
Endif
and add the actual BrwDeCode() procedure at the end of browser.prg:
*******************************
Procedure brwDeCode(toBrowser)
	Local lnWA, lnFrom, lcHeading, lcObjects, lcCode, lcRet
	lnWA=Select()
	Select (toBrowser.cAlias)
	Go toBrowser.aClassList[1,2]+1
	lcDeName=ObjName
	lcClassLoc=IIF(EMPTY(classloc), "", [of "]+FULLPATH(classloc)+["])
	TEXT textmerge noshow to lcHeading
*****************************************************************
Define Class De<> as <> <>
<>
	ENDTEXT
	lcHeading=lcHeading+CR_LF+CR_LF
	lcObjects=""
*	lcDEName=objname
	Skip
	Scan While Recno()<=toBrowser.aClassList[2,2]-1
		TEXT textmerge noshow to lcObjects ADDITIVE

	Add Object <> as <> with ;
<>

		ENDTEXT
		lcObjects=lcObjects++CR_LF+CR_LF
	Endscan

	Go toBrowser.aClassList[1,2]
	lcCode=""
	Scan While Recno()<=toBrowser.aClassList[2,2]-1
		lcMethods=toBrowser.FormatMethods(Methods)
		Do Case
			Case 		EMPTY(parent)
				lcFullParent=""
			Case Lower(Parent)#Lower(lcDeName)
				lcFullParent=Parent+"."+ObjName+"."
			Otherwise
				lcFullParent=ObjName+"."
		Endcase

		lcMethods=Strtran(lcMethods,"PROCEDURE ","PROCEDURE "+lcFullParent)
		TEXT textmerge noshow to lcCode ADDITIVE

	<>

		ENDTEXT
	Endscan

	Go toBrowser.aClassList[1,2]+1
	TEXT textmerge noshow to lcRet

**************************************************
*-- Dataenvironment for:         <> <>
*-- ParentClass:  <>
*-- BaseClass:    <>
*-- Time Stamp:   <>
*

<>

<>

<>

ENDDEFINE
**************************************************
	ENDTEXT

	Select (lnWA)
	Return lcRet
Endproc
To have this in your current object browser, you also need to compile it:
BUILD APP (_browser) FROM  HOME()+"TOOLS\XSOURCE\VFPSOURCE\BROWSER\BROWSER"
Next time you export code from the Class Browser, it will contain the additional code that this builder can use.

Source code

Dragan Nedeljkovich, Now officially retired
After (and/or along with) playing with Sinclair ZX Spectrum and Atari ST, I left teaching in 1986 and went through a series of machines and operating systems: CP/M (Cobol, Turbo pascal, CB-80 Basic), PDP and VAX (Cobol, Basic Plus 2, scripts), DOS 3.1 through 7.0, Windows 3.1 through XP. Ran several (almost) forgotten networks - RPTI, Lantastic, WFWG, Novell 2.x, 3.x, 4.x, Windows peer-to-peer and eventually NT and just plain IP (Win/Lin mix). After a brief flirt with Clipper in 1988, discovered Fox and went through mFoxplus 2.1 (DOS), Foxplus (Xenix), FP 1.01, 1.02, 2.0, 2.6, VFP 3.0 (briefly), 5.0, 6.0 (SP 1 to 5), 7.0 SP1, 8SP1 and now 9. Maintained one framework, created another (in DOS Fox), helped create yet another one, then used a few more - bought or in-house - frameworks. Always too lazy to type the same sequence more than four times, I'd rather write a builder or some Intellisense. Along the way, promoted a dozen beginners into experienced programmers. IOW, been there, done that, the T-shirts are recycled already, and it's still interesting and fun way to live. Retired in 2019 and finally found time for those pet projects... in Dabo/Python.
More articles from this author
Dragan Nedeljkovich, January 31, 2007
Generally, datasessions serve this purpose - they create an isolated environment where our code can run and use tables, cursors etc as it pleases, without disturbing other code. Sometimes wee need a little more granularity - to have a routine which may create any number of cursors, open additional t...
Dragan Nedeljkovich, November 27, 1999
As described in the help, padr('abc',20) will give a string with 17 trailing blanks - but try padc(12, 13), or padl(date(), 20) - you will get a string containing the string representation of number 12 or today's date, padded with blanks (or any other string you choose) on the side of your choice. ...
Dragan Nedeljkovich, November 5, 2000
There's a problem of the DataEnvironment which exists in the .scx but not in the .vcx, so when a form is createobject()ed, it has no DE. There are several ways to create one - either in the form designer, or using a session class, or simply opening the tables in code and avoiding the DE altogether (...
Dragan Nedeljkovich, December 2, 2002
This FAQ describes some considerations to take care of when using spaces in directory names when being used with macro substitution.
Dragan Nedeljkovich, September 1, 2006
Following a thread on the Universal Thread, Dragan Nedeljkovich wrote this article on how to make the totals below the grid scroll horizontally together with the grid. Sometimes, we only wish we wrote this much earlier. This article describes a way to calculate those totals, creating fields in some ...