Cesar responded to my recent walkthrough on barcharting with some VFPReport-specific questions. Here is his first one:
At this moment, I'm doing something like this:
SET REPORTBEHAVIOR 90
PRIVATE oFoxChart && needed by report
oxFoxChart = ThisForm.Foxcharts1.ChartCanvas && image control
REPORT FORM FoxChartsView PREVIEW
In my report, in the "ControlSource" I'm passing the "oFoxChart" reference, and it gets printed.
Is that the best way to do that?
In my opinion, probably not.
It's really hard to tell, because (as I told Cesar) I don't have a lot of time to investigate his use of the image control. I don't really understand why this chart-handling object should be scoped to a form, for example.
My first question to Cesar was: why aren't you putting this object in the GFX collection?
Cesar answered, "Oh, sorry, I didn't get what you meant here. Could you recommend me somewhere to do some researches regarding this?"
Start with the TMM docoid on FXand GFX Inteface Implementations. It takes only one method implementation to become part of the appropriate collection. Read the TMM docoid covering the ReportListener Decorator Foundation Class, which hosts the FX and GFX collections. Read the Data Visualization in Reports with VFP 9.0 SP2 article, in which Colin and I thoroughly worked through a GFX scenario.
Check out the TMM docoid on GFX Page Region Export, while you're at it. The sample code section states that it is an excerpt from code in "a custom graphing object", which should give Cesar some idea that this approach actually works for what Cesar has in mind.
In point of fact, the example code in that topic has nothing to do with graphing, it is just illustrating one of two ways for a graphing object to interact with and leverage an Output Clip (or "Page Region Export") object, so he might not be interested, but there are other types of synergy between the code for this gfx object and his work, so, hey, check it out. Maybe I'll use gfxOutputClip to illustrate some other ideas in a followup post, in which I'll try to address some of Cesar's other questions.
The trouble is, I'm kind of embarrassed to be having to suggest this stuff. It's not like we hid our recommendations; we first published these suggestions in the helpfile that shipped with RTM. In SP2, we implemented production-quality examples of our recommendations and shipped them with the product.
Hence the application of the Duke Principle; more about that below. For now, let's just get some work done.
Let's roll up our sleeves
Let's assume that Cesar has really good reasons to tightly couple his charting object to a form or form class. For example, let's suppose the data required by the chart isn't actually available during the run of the FRX, only to the form.
This problem -- how do you scope variables for a report -- has more general application than Cesar's question. That gives me an excuse to explore it in a bit of depth. There are a number of different interesting things to be learned from playing with this question, so, using the image control scenario as context, I am going to build up an answer in "layers".
The basic principles we're going to explore include:
- some specific-to-FRX capabilities you can use
- some ways that you should treat FRXs more like full-fledged citizens of your application, even though they are "object-assisted" rather than "object-oriented" elements of the application
- some concerns about where and how code should be bound, including how to avoid these runtime requirements getting in your face while working on report design
A specifics-free report for testing
Being extremely lazy, I use a report that doesn't require any specific data set for testing like this. The report expressions are simply FIELD(1) in the header and EVAL(FIELD(1)) in the detail line. For this scenario, I'll just add an image control into the detail band.
All the reports in the sample code for this post have this same setup. If you have a table open and SELECTed, they will use it, and if you don't they will do the equivalent of a USE ? when you run the REPORT FORM command. Any table will do. (You'll see that I'm using an old table from the Europa beta test in these screen shots.)
The next step is to have some mechanism that emulates Cesar's requirement in a context-free way way. While (again) this isn't the way I normally do things, Cesar is using an image-based object reference as the expression-type control source of the image layout control in the FRX.
To emulate the dynamic nature of Cesar's charting class in this test report, I'll create a simple alternation of two images, chosen randomly (by you) at the beginning of the report run. It works like this:
Each report has two report variables, MyImage1 and MyImage2.
Both variables are released at the end of the report run.
Both variables request the name of a file from you during report initialization (to ensure different images, one variable asks for a JPG and the other asks for a GIF).
Both variables store their own names to themselves after initialization. This is a trick I often use for a variable I want to use but the report engine doesn't "understand" or adjust directly.
So now we just have to tell the the report to alternate images for each detail line to show some dynamic behavior as a proxy for Cesar's real object.
We'll do that very simply by adding some code into the FRX event that runs before the detail band.
Remember I have no idea what triggers Cesar's chart to start calculating and when to close up shop, this is just an emulation; it doesn't really matter what
images show, or how they get there, as long as we can demonstrate that they get there during the report run.
OK so far?
Are you perplexed at the use of EXECSCRIPT() in the On Entry code?
Why didn't I just make the assignment: MyVarName.PictureVal = IIF(RECNO() % 2 = 0, MyImage1,MyImage2) ?
There's a really good reason: the assignment would be "read" by the On Entry code as an evaluation, a simple .T. or .F. result. The actual contents of the .PictureVal property would not change.
I could have used a UDF, of course, I'm just binding code that belong to the report directly to the report to save confusion when this report is moved around. You can easily place the code elsewhere in your environment.
All the reports in the sample code for this post have this same code. They only differ in how they assign the image control variable, which is the only part of the reports that is actually germaine to Cesar's question.
Step 1: Hey, I know: let's use a report variable!
Report variables are durned useful things. You should be able to see that, even if you don't customarily use them, by my use of MyImage1 and MyImage2 in the generic setup code you've seen in these reports so far.
To serve as the container for the image-based object reference for the image layout control in the report, you can use another one. I've created one, initialized similarly to MyImage1 and MyImage2, to set up a simple base-class image object as proof-of-concept, in FRXwithVars.FRX.
The control source for this first report is, of course, the simple expression MyVarName.
I then adapted this simple approach to handle Cesar's FoxChart custom object instead of a baseclass image control, in FRXwithVars1.FRX. I create a FoxChart proxy object in a PRG called CreateFoxChart.PRG, as you can see in the NEWOBJECT call in this report. Then I address MyVarName.ChartCanvas instead of MyVarName in the On Entry code and the image control source:
This is a really nice approach if the data and logic for FoxChart is completely available to the class during the run of the report. You've bound in the requirements for the report directly to the report. There is no need for the report to "touch" anything, such as a private variable, that is not directly visible to the person creating or maintaining this FRX.
As a result, you can run both FRXWithVars.FRX and FRXWithVars1.FRX from a simple REPORT FORM command, no wrapper code, and they run fine if you choose to preview from the Report Designer.
You can even run them with SET("REPORTB") = 80 or 90, for quick Designer previewing, if you want. The image control source works when you preview. But the image object control source stuff is only guaranteed to render properly to the printer when you work in object-assisted mode. You knew that, didn't you?
Step 2: Attaching to a form with a little help from the forms stack
The approach in FRXwithVars and FRXwithVars1 works great if your FoxChart (or similar) object isn't tightly coupled to your form or form class. Never mind that I don't know why it would be or should be -- suppose it is?
Suppose Cesar has some really good reason to initialize the FoxChart object in the outlying form, and give it special instructions, before running the report? How does your report code "grab" the object reference?
Well, my friend, your report can do this quite easily and reliably by accessing form stack.
At the time the report engine generates output, the form that invoked the report will either be the active form (WONTOP(), in OldSpeak) or the next form down in the stack. The difference is simply whether a Fox form has been used to show report status during the run. If SET("REPORTBEHAVIOR") = 90 and you've invoked one of the FFC ReportListeners without setting QuietMode = .T., and without using the NODIALOG keyword on the REPORT FORM command, the first form in the stack is the "therm" or progress window.
To illustrate this, I've included the not-very-interesting CreateForm.PRG and two more report forms: FRXWithForm80.FRX and FRXWithForm90.FRX.
These two reports still have the report variables MyImage1 and MyImage2, which are just part of the test setup. They no longer need the report variable MyVarName holding the image object reference, since they address the forms collection directly.
The only difference in these two reports is whether they address _Screen.Forms(1).FoxCharts1.ChartCanvas or _Screen.Forms(2).FoxCharts1.ChartCanvas in their code.
As CreateForm.PRG points out in its comments, you could make the appropriate index value a public member of your application object, add a property to screen, or perform similar tricks to avoid having two separate reports for these scenarios. I'm just illustrating different ways to reference a form and its potential FoxChart object member here, not providing a recommendation on best practice.
Why not? Because I don't really care. You should have a method for sharing information in your application environment. I don't care what it is. You might have a private data session with a cursor holding this type of value, for all I know. The point is your report code should be able to use it, just as your other application components use it. Don't assume reports are a special case.
One thing I really don't like about the code you see in CreateForm.PRG and its reports, besides its obvious clumsiness, is that the two reports won't run easily during report design.
How do you suppose we can take care of that?
Step 3: Hey, I know -- why not use a report variable?
You can put the two approaches you've seen so far together. You can fit an FRX into your general application approach, and you can solve design time issues, if you put a report variable, a little code, and the forms stack together.
In my example for you, I've demonstrating using an application object attached to _SCREEN. But once again, I don't care how you manage your application scope, or forms collection, etc, in your work. Whatever you normally use is fine and you can adapt this approach to suit.
Consider FRXWithVars2.FRX. This time, I've put the report variable MyVarName back into the report. The control source for the image in this report is, once again, the simple expression MyVarName, as it was in FRXWithVars.FRX (the first one), nothing more exciting. The Detail band On Entry code is also back to the simplest version, the one we used in FRXWithVars.FRX. Nice and simple.
In FRXWithVars2.FRX, initialization of the MyVarName report variable changes. If we can find an application object, we'll ask it to hand us a the ChartCanvas belonging to the FoxChart instance it deems appropriate. If it can't find an appropriate FoxChart instance -- never mind what its rules are -- it will return a base image object. And if we can't find an application object at all, we'll stub in any old image control:
IIF(TYPE("_SCREEN.MyApplicationObject") = "O",
As you can probably tell, this report will run fine in design mode, whether there happens to be an application object defined or not. It just won't show any interesting behavior that is specific to FoxChart.
I've defined MyApplicationObject in CreateForm2.PRG. When this object is available, its GetFoxChart() method works out the correct form from which to derive the FoxChart object, using the forms stack or whatever method you prefer, defaulting to a base class image for testing purposes if nothing else turns up:
DEFINE CLASS MyApplicationObject As Custom
LOCAL oCanvas, iForm
FOR iForm = 1 TO _SCREEN.FormCount
* you can use a more sophisticated
* method to figure out whether you
* have an object of the appropriate
* type, such as checking the
* class and asking it for the reference;
* the point is to approach
* the stack in the correct order
* or do whatever you feel appropriate
* in your environment
WAIT WINDOW TIMEOUT 2 "Found!"
oCanvas = _SCREEN.Forms(iForm).FoxCharts1.ChartCanvas
IF VARTYPE(oCanvas) # "O"
oCanvas = CREATEOBJECT("Image")
So now we have something that runs well in design time, or from a bare REPORT FORM ... command, no matter what your current environment, and is also integrated with your standard app development practices, whatever they are.
Where does the GFX collection fit into this scenario?
The GetFoxChart code in your application environment could just as easily be checking to see if a GFX FoxChart member was available and put one in, if not. It could be setting up this member for the current form's requirements, if necessary. You could, in fact, be calling it from the form before the REPORT FORM command, so that the form could communicate directly with the application manager object, passing a reference to itself, and avoiding the need for the manager to go through the form stack.
You're going to ask: how does that help you scope your image control reference? There are a lot of ways to do it, once your charting object is swimming "inside" the FRX sea. For one thing, you don't actually need the image control at all, you can draw directly to the surface of the page. For another, if you really like that image control, you can simply use a bare image object with a report variable and CREATEOBJECT() or NEWOBJECT(), as we've done in previous examples, have your gfx object interrogate the FRX details to figure out what it's supposed to write to, store a reference to that object in its ChartCanvas member.
You can bind additional report instructions to the image layout element using MemberData -- now that I think about it, you could probably even swap out that image reference stubbed in by the report variable for a more specialized one, on the fly, during BeforeReport.
Support your charting object is supposed to take care of drawing to images on forms as well as to reports? That's no problem. All you need to do is add one method that is report-event-specialized, and that invokes your "standard" methods appropriately during the run.
What else can we add about scoping code and variables for report use?
One thing I used to do a lot was to leverage the Data Environment object to "hang" code and member objects on. As you may or may not be aware, you can use the NAME clause on a REPORT FORM command, just as you can on a DO FORM command, to give yourself a handy object reference. Because (here we go) reports are object-assisted, not object-based or object-oriented, the reference you get is a reference to the associated Data Environment object for the report. This reference exists even if you put nothing in your DE for the report, don't use a private data session, etc. It's always there.
You can (for example) use the Init code code of the DE to look through the forms collection for a FoxChart type object, or a form of appropriate class. You can AddProperty to the DE to give yourself information about whether you had to initialize some objects (and whether you want to release them later, accordingly, or whether they existed before the report began.
You can refer to these elements in report variable and report expressions, by using the NAME you provided to the REPORT FORM command. To make everything nice and tidy, you can even use DE initialization code to STORE THIS.Name TO a report variable, so that the current NAME is known, no matter what the REPORT FORM command happened to use.
The DE was the first object to "assist" report runs, a long time before VFP 9. It used to be a lot of fun to use it. In my first experiences with image control sources I did in fact "hang" these references off the DE.
I don't do this so much any more, for two reasons:
- There's some instability in memory handling when you're designing a report (and, often, making code errors) that can corrupt DE code. It's okay once you're done, but if you crash while working on a report and adding these fancy features, and if you forget to take frequent backups, the results can be... most annoying.
- It really goes against the grain to treat reports as a special case within the application, and to use the primitive DE object, when we have so many better ways to attach object syntax to reports now. Let's use what we have to drag and drop reports, if not into a class library, at least into the modern era.
That's why we have report listeners. That's why we have extended syntax in the FX and GFX collections. That's why the FX MemberData Script Implementation lets you attach any script you want, to any element of a report, in any event, and that's why Reporting Member Data lets you attach any custom attributes to any element of a report.
Which brings me to...
The Duke Principle
A long time ago, C and I worked with a truly delightful guy named Duke.
His name wasn't really Duke, but I'm going to call him Duke because it is the name everybody (including his mother) calls him. And, before you ask, he's not a Fox programmer and he is highly unlikely to read this blog. However, in case he does, he should know that we think of him fondly and often.
Anyway, the three of us were staying at a hotel at some point, and we were sitting in the little hotel restaurant, having some drinks and thinking about what to do with our evening. Duke motioned to the waiter, who hurried over, menus in hand. And Duke asked him "Excuse me, do you have any idea where we could get some good food around here?"
The waiter looked at him, completely non-plussed, and tried to decide what he could possibly say. He finally just walked away, I think.
Now, Duke was not usually an insensitive person in the slightest. But we were sitting in a restaurant. I'm going to leave you to imagine how the waiter felt being asked that question.
I'm also going to leave you to figure out what that might have to do with this post.
This has been a long post, and that's enough for now. I'll get to Cesar's other questions in some followup post.