Extending the Report Preprocessing Idea

When I wrote my two part walkthrough on using preprocessing to provide information to a report under programmatic control, I used the idea of "group page totals" as a practical example. I hope that the use of this example doesn't give you the idea that group page totals are the only valuable use of report preprocessing. 

As a reminder, it was a "two part walkthrough" because, after you've handled all the programmatic details in the report-serving engine, you still have to invoke the report twice, in report client code, to make the "second run" appear seamless to your user.  That's what part #2 of the walkthrough does.

There are probably lots of times you would want to have all the information from a report available to you and then evaluate that information before going on to the "real" report output run. 

Some of them are pagination related, such as the "group page total" idea I used in  my example.  You use the report engine to paginate, appropriately for the rendering extension (such as PDF) that you really want, and then scoop up those page number values to use them for your own purposes on the second run. 

Other uses for preprocessing might be data-related.  While I'm not thinking about this terribly hard right now, when the preprocessing is data-related rather than page-format related, your best preprocess output might the XML export data format, which gives you all the report's evaluated data content in an easily-reference-able form during the second run.

Even the group page total example preprocessing output, in and of itself, is extensible, as you'll see in this post.

No matter what you do, please don't get the idea that my illustrative code is the end of what you can do. Once you understand the principle of storing these values externally, whether in XML or in a database, for reference in the second run, you have many opportunities.  

My previous post mentioned that you could use the information in a table of contents or an index, and Joe Carey wrote to ask about this:

I'm trying to create a index of group header on the last page of a RS report is this possible? since the page numbers can only be got from the header/footer and the footer can only be a fixed size ? what if my index list spans multiple pages ??

I would be very very grateful if you had any ideas to solve this…

Well, as a matter of fact I do <g>, probably didn't even need two "verys" to be willing to help, especially considering the fact that the XML (or sql data) that you need to store as described in my last post, is already sufficient for Joe's purposes. We already have the information we need. How hard can it be?

Getting it into the report is another story. 

Joe indicates that he thinks this should be in the footer — it definitely should not.  As Joe does know, you'd be in trouble if you had a lengthy index.

The right trick here is to create a second table in the report.  So, this time let's take a close look at how that would work.

Here's something you can do

I would probably want to set "page break before" on this second table, because it seems like a good idea to start the index on a separate page — but that's not required. Just add a second table to the layout, for starters, below the first one that holds your "real" content.

If you would like to use the same dataset with this second table, you can set this table's groups up pretty much the same way as you did your original table.  This time, suppress detail lines.  Alternatively, since the group information is actually the detail you want for this table, you can use what RS calls a "detail group". If you choose this approach, you have no group headers or footers at all. FWIW, I find that "detail group" terminology very confusing; another way to express this is that we are creating a "summary table" for this particular dataset. 

As a third alternative, you can create a second dataset expressly for this purpose. In the second dataset's query, you only need a SELECT DISTINCT or GROUP BY to provide the group header values you need for the use of this table.

Whether you use the original dataset, or a second dataset, be sure to explicitly associate this table with the correct DataSetName property value to match your choice.

Also — this is critical — if you originally used the Wizard to create your report, you probably have your original table inside a List element, which is also associated with the same dataset. 

Don't put the new table inside that List element! Put it below the List, so that you are moving through the dataset independently while processing the second table. 

It's kind of hard to see. So, after you have gone this far, preview and go to the end of the report. You'll know if you've done this wrong (mistakenly put your second table inside the List holding the first table) if your second table has only one row in it, corresponding to the last group in your data. (You'll find that each of your original group-determined table breaks have a similar second table, with one row, right after them.)

In my example data in the walkthrough, my group page-break information was on Regions within Continents.  To show you what a "detail group" looks like, I chose to create a detail group in my second table.

Detail Grouping in second table 

Now that you've done this, you're going to add an expression in a column of this second table that looks something like ="p" & Code.GetGroupFirstPage(Fields!Region.Value.ToString), which of course is going to evaluate to the right page number.

The new GetGroupFirstPage function reads the same XML file we've created previously.  It looks something like this:

Function GetGroupFirstPage(ByVal currentGroup As String) AsObject
Dim g AsString, xa As System.Xml.XmlAttribute, thisLastPage As Integer
Try
   If doc Is Nothing AndAlso (Not GlobalPreProcess) Then
      ' shouldn't happen on the second pass
      ' if we don't get rid of the doc at the end of the previous table
      ' but we expect this on the first pass.
      groupNo = 0
      doc = NewSystem.Xml.XmlDocument()
      doc.Load(fileName)
   End If
   If
doc Is Nothing Then
      If
GlobalPreProcess Then
         g = "[0]"
      Else
         g = "NO DATA"
      End If
Else
      groupNo += 1
      xa = doc.SelectSingleNode("/root/Group[@current='" & currentgroup & "']/@lastPage")
      If xa  Is Nothing Then
         g = "NO DATA"
      ElseIf groupNo = 1 Then
         g = 1
      Else
         g = CInt(xa.Value)
      EndIf
   End If
Catch
Exc As Exception
   g = " error reading: "& Exc.Message()
End Try
Return g

End Function

If everything goes correctly, your report results in this second table are exactly what you want: 

Group Header Index page

But is it really just that simple?  Not on your life.

It's either much, much worse, or much, much simpler — depending on what your individual requirements are.

Things … not so solid.

As you can tell from the screen shot above and the code, I really did work out a proof-of-concept for Joe, and I absolutely did not change the simple XML format I was writing to get this result as part of my original SQLWorld example report. 

However, if you look at the function you'll see some new variables used. In reality I had to do some work on the original functions that were writing the XML file and handling the group page numbers, altering them from the functions I originally posted, to do this. 

For example, in this version I could no longer rely on the last page of the report, using Globals!TotalPages, to tell me when I was on the last group for the report.  I also had to account for different behavior in the last page(s) of the report to get the right page header expression. You can see that the second line of the page header information, which handles group level page numbering, is suppressed on the Index page, in the screen shot above. What you can't see is the amount of code that it took to get this to work properly.

Here's what you still have to do

Why don't I just publish revised versions of these functions and be done with it?  For one thing, because I'm not convinced I wrote the "right code".  I threw something together so that the possibilities are clear, but I can see that my code isn't clean enough to recommend just because it appears to be working. 

For another thing, once I decided that this wasn't a publishable version, I didn't go far enough to see if the kludges I was throwing in would have to be adjusted for use in the PDF renderer.  As you may recall from the walkthrough, there is a lot of code that I put in to handle the fact that the various renderers may handle the sequence of events, paginating and calculating page contents, differently, so what works for HTML doesn't necessarily work for PDF.

For a third thing, Joe might be able to use much simpler code than this, if he chooses to write the XML documents during the run something like what I've described, but he doesn't need the special group page total contents in each page header. 

In other words, he could write the XML document (or a collection) during the run, and then use his results in the second table.  It's possible, even, that no preprocess run might be required in Joe's case, PDF and HTML might work the same way, etc.

I'm not going to go ahead and test every permutation of this. I hope Joe, and other people, get some good ideas about why you might have multiple tables in a report — a good, meaty subject in itself — and some more good ideas about how preprocessing, as well as additional types of careful attention to the sequence of events during a report run can be useful.  Enjoy!