Everting the PrintDocument paradigm

Collapse
This topic is closed.
X
X
 
  • Time
  • Show
Clear All
new posts
  • Frnak McKenney

    Everting the PrintDocument paradigm


    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: Gedankenexperim ent):

    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 "checkpoint ing" 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 ( !ReportTokenize d ) { this.GenerateRe port(e); }
    PageNumber++;
    while ( token = getNextToken() )
    if ( token.Type = TokenType.EndRe port )
    {e.HasMorePages = false; return; }
    else if ( token.Type = TokenType.EndPa ge )
    {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
    "checkpoint s" 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,ro w,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
    --
Working...