Evert: verb. Turn inside out; turn the inner surface of outward.
The project I'm working on involves seven or eight reports. They
vary from simple ("print this field here, that there, with a header
and footer") to more complex ("put field A on the left unless FieldB
is nonzero, in which case add it to a bucket and print it at the end
of the report").
Writing the code to put the data onto a GDI(+) "page" isn't too
bad... or it wouldn't be, except that I have to stop and start my
logic to accomodate the Win/.NET PrintDocument printing paradigm.
This peering-out-from-within worldview dictates that my
report-generating code must be (generally speaking) "interruptible",
be ready to "save its state" and exit the OnPrintPage routine any
time I finish filling a page. This, in turn, means that both the
code to lay out the report and the code to define the content of a
report become inextricably intertwined with the code used to slap
the report onto the page. Bleah! It's enough to make someone want
to punch a 1403 carriage-control tape.
I've spent more time than I should trying to find someone who has
figured out how to get around this procedural inversion (from my
point of view, anyway <grin>). I still haven't found a perfect
answer that can handle any procedural code I'd care to throw at it,
but I have found three very good efforts in this direction:
[Warning: The following comments are based on my own
interpretation of articles, code, and documentation. If I
have misinderstood anything, or accidentally misrepresent it
in the following, I offer my apologies in advance.]
From The Code Project (
www.codeproject.com)
Daniel Zaharia's "Report Builder"
Mike Mayer's "Printing Reports in .NET"
From MSDN's Smart Client Applications (msdn.microsoft.com)
Duncan Mackenzie's "Printing Reports in Windows Forms"
Mackenzie alludes to the "state" problem when he says "...you need
to keep track of a few details, such as the current page number and
the current row of data..." This is a problem all three address in
slightly different ways. Mackenzie and Zaharia base their reporting
on data objects, so their "state" requirements are fairly simple;
Mayer uses a set of classes to track the current section of the
report and which row is being printed.
All of these (and I apologize for my cavalier treatment of three
solutions which _do_ work, and which represent a great deal of
effort on the part of their authors) simply adapt to the current
situation. I'm feeling a bit stubborn, and I'd like to see if there
is any good, general way of making a PrintDocument "handler" adapt
to _my_ way of coding rather than my adapting to its. And what I
want to do is define headers and footers, then proceed to throw
lines and columns of whatever out until _I'm_ done, and only then
declare that the report is complete. I don't want to worry about
where page breaks occur, I want some hidden layer of software to
worry about it instead.
As I see it, any solution has to find some way of preserving the
state of _my_ report-generating code so it can be invoked from (and,
at the appropriate times exit to) the PrintPage event handler code.
I've come up with two general ways of approaching the problem (see
also: Gedankenexperiment):
1) Generate the whole report in the first invocation of the
PrintPage event and store it as a set of "tokens" in a FIFO
stack (or serialize the tokens into a memory or temporary file
stream), or
2) Develop a very general way of "checkpointing" the state of
arbitrary procedural code so I can invoke my return to the
PrintPage event handler as if I were calling a subroutine.
Method 1 appears to be doable. The "tokens" would all be created on
the first invocation of PrintPage (since that's the first chance the
code would have to access the printer's Graphics object). Each
token would contain all the information required for (say) a
guaranteed-to-fit-on-one-page GDI(+) DrawString invocation,
including the Font, Brush, and StringFormat information plus the
string itself. The drawing rectangles would be pre-calculated. The
only thing left to do (aside from Headers and Footers) would be
filling in page numbers for things like "Page %p of %q".
The PrintPage event for Method 1 would look something like the
following:
int PageNumber; bool ReportTokenized;
BeginPrint { PageNumber = 0; ReportTokenized = false; }
PrintPage(...) {
if ( !ReportTokenized ) { this.GenerateReport(e); }
PageNumber++;
while ( token = getNextToken() )
if ( token.Type = TokenType.EndReport )
{e.HasMorePages = false; return; }
else if ( token.Type = TokenType.EndPage )
{e.HasMorePages = true; return; }
else
{ invoke e.DrawString...}
}
// If the token stream has been constructed properly,
// we never reach here.
}
The only report-specific code is encapsulated into a per-instance
GenerateReport() routine.
I _think_ that if I use "tokens" for EndPage and EndReport (and
possibly a PageNumber token) the only "state" information I need to
worry about is whether my amazing "getNextToken()" routine resumes
properly on each pass through the PrintPage event handler. If I
used one (huge) string to store my tokens in, for example, all I'd
need would be an index pointer. (Well, that and a _lot_ of working
storage for partial strings <grin>).
Points of concern: If I serialize to a Memory stream, how "big" a
report can it handle? If I serialize to a temporary File stream,
what performance hit could I see for small reports?
Method 2 is a lot messier, and I can only see bits and pieces of an
approach. Since "Design Patterns" seem to be all the rage these
days as vehicles for expressing concepts, this seems a good time to
ask if anyone is aware of a .NET-implementable design pattern for
what was one called "co-routines". We want the PrintPage event
handler to run for a bit, then invoke our report generator for a
bit. The report generator produces one page's worth of output, then
"checkpoints" its state and transfers control back to the PrintPage
handler.
So far, so good. We can serialize and stuff away all sorts of
variables and restore them when our report generator gets
(re-)called for Page 2. The hairy (a.k.a. "I haven't figured this
part out") bit is that, even with all these variables set to their
old values, I don't see any general way of "jumping" into the middle
of (e.g.) four nested loops and picking up where we left off just
before we went ended the last pass through PrintPage.
Here's an outline of how it might be done. First, a general (but
all-encompassing!) non-initializing looping construct would have to
be created. This gets a bit ugly (limited imagination on my part,
most likely):
if (!Restarting) { page=0; section=0; row=0; column = 0; }
else {unserialize page,section,row,column }
for (page < npages; page++) {
...
for (section < nsections; section++) {
...
for (row < nrows; row++) {
...
for (column < ncolumns; column++ ) {
...
}
}
}
}
Trouble is, when I try to generalize this to a report with a
two-column section followed by a three-column section followed by a
couple of multiline text Notes, my head starts to explode.
_Is_ there a simpler way of achieving the equivalent of a light-duty
checkpoint-restart for code? Or is Method 1 (tokenizing the report)
the only really practical way of implementing a general
report-generating paradigm while avoiding a lot of special-cased
code?
Okay, end of rant. It's possible that this topic has already been
discussed to death at some point and I've simply missed it in my
searches; if so, please feel free to skip on to the next USENET news
item with my apologies. In any case, comments will be welcomed.
If anyone comes up with any way of adopting Method 2 -- which
_could_ be much cleaner -- I'd love to hear from you. Conversely,
if anyone knows a reason or reasons why why Method 2 Definitely
Cannot Be Done Using .NET Framework v1.1 I'd also appreciate knowing
about it.
Meanwhile, I need to get back to work on my project. I'll see how
far I can get using Method 1 (wish me luck -- I think I've
inadvertently reinvented the Windows MetaFile, or at least its third
cousin, twice removed <grin>).
Frank McKenney, McKenney Associates
Richmond, Virginia / (804) 320-4887
Munged E-mail: frank uscore mckenney ayut minds pring dawt cahm (y'all)
--
"The probability that we may fail in the struggle ought not to
deter us from the support of a cause we believe to be just."
-- Abraham Lincoln
--