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
ADD OBJECT form.pageframe1.pgnew.obizevent AS bizevent
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
IF NOT EMPTY(lcClassLoc) lcParentClass = lcParentClass + " of "+CHR(34)+lcClassLoc+CHR(34)+" " ENDIF
Do Case Case Not llHTML lcAddObject=lcAddObject+lcParentClass
If Not Empty(lcClassLoc) And Not toBrowser.cFileName==lcClassLoc lcAddObject = lcAddObject + [ of "]+lcClassLoc+["] Endif
lcCode=Strtran(Strtran(lcCode,MARKER,""),CR_LF+Tab+CR_LF) Do While Left(lcCode,1)==Tab lcCode=Alltrim(Substr(lcCode,2)) Enddo
If llSCXMode Go toBrowser.aClassList[1,2]+1 If Class="dataenvironment" lcCode=lcCode+CR_LF+CR_LF+brwDeCode(toBrowser) Endif Endif
******************************* 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
BUILD APP (_browser) FROM HOME()+"TOOLS\XSOURCE\VFPSOURCE\BROWSER\BROWSER"
Source code