November 2005 - Posts - Matt Faus

InfoPath Dev

Matt Faus

November 2005 - Posts

  • Repeat Table Headers Across Multiple Printed Pages

    InfoPath is a great tool for collecting and organizing data, but it lacks when it comes to print formatting. For example, when a dynamic table with a large set of rows must be formatted to print across several pages of a report, the column headers should appear, for better readability, at the top of each page containing this table. Otherwise, you have to flip back to a previous page each time you needed to reference the column names. Furthermore, rows should not break across two pages (i.e. part of the row on one page and part on the next).

    In this task we will use code to create table headers that repeat across multiple printed pages. We will also explain how to expand your code to prevent rows breaking across pages. We will accomplish this using a repeating section which contains a page break and a repeating table. This structure, along with our code, will dynamically create page breaks so that table headers appear at the top of the page. To demonstrate this technique, we need a data source with plenty of rows.


    Copy the following code into a text editor, and then save the file as Vehicles.xml. This file will be used as a secondary data source in our form.

    <?xml version="1.0" encoding="UTF-8"?>
        <Vehicle year="1963" make="Chevrolet" model="Corvette" description="Features:
        <Vehicle year="1963" make="Chevrolet" model="Nova" description="Features:
        -2 year warranty"/>
        <Vehicle year="1963" make="Porsche" model="356" description="Features:
        -Very fast
        -Impressive body design   
        -3 year warranty"/>
        <Vehicle year="1963" make="Porsche" model="Carrera" description="Features:
        -Plenty of room
        -3 year warranty
        -Free tires"/>
        <Vehicle year="1973" make="Ford" model="Bronco" description="Features:
        Towing power
        -High road clearance
        -Free tires
        -3 year warranty
        -Mud flaps"/>
        <Vehicle year="1973" make="Ford" model="Mustang" description="Features:
        -Great sound system" />
        <Vehicle year="1973" make="Ford" model="Pinto" description="Features:
        -Beautiful finish
        -3 year warranty
        -Extra mirrors"/>
        <Vehicle year="1973" make="Toyota" model="Celica" description="Features:
        -Good gas mileage"/>
        <Vehicle year="1973" make="Toyota" model="Land Cruiser" description="Features:
        -Tire upgrade options
        -Cruise Control
        - Off-road capabilities"/>
        <Vehicle year="1979" make="Jaguar" model="XJ6" description="Features:
        -European style headlights
        -3 year warranty
        -Fun to drive"/>
        <Vehicle year="1979" make="Pontiac" model="Trans Am" description="Features:
        -2 year parts and labor included   
        -Financing available"/>
        <Vehicle year="1979" make="Volvo" model="240" description="Features:
        -European style headlights
        -3 year warranty"/>
        <Vehicle year="1979" make="Volvo" model="244DL" description="Features:
        -Great sound system
        -Great gas mileage
        -3 year warranty
        -Fun to drive"/>
        <Vehicle year="1987" make="Dodge" model="Ram Charger" description="Features:
        -High safety rating
        -3 year warranty
        -Fun to drive around very tight corners and up slippery slopes filled with ice"/>   


    Now that we have our secondary data source, let's create a new blank form with two views: a default view for data entry, and a print view that repeats the table header across pages. We create two views so that data collection is easier, and so that InfoPath only has to format our print view upon switching to that view as formatting might take some time. We, therefore, want to run this code as infrequently as possible.

    Add the secondary data source to a form:

    1. Design a new blank form.
    2. Choose Data Connections from the Tools menu, and then click Add.
    3. In the Data Connection Wizard, select Receive Data, and then click Next.
    4. Select XML Document, and then click Next.
    5. Click Browse, locate and select the Vehicles.xml file, click Open, and then click Next.
    6. Click Finish, click Yes, and then click Close.

    Design the data entry view:

    1. Open the Data Source task pane.
    2. Choose the Vehicles from the Data Source drop down list.
    3. Drag-and-drop the year attribute into the view. This will create a repeating section with the year text box inside.
    4. Drag-and-drop the make, model and description attributes into the repeating section, and then resize the fields as shown in Figure 1.
    5. On the Display tab of the Text Box Properties dialog box, select Paragraph Breaks, Read-Only, and Wrap Text, and then click OK.

    Figure 1. The completed data entry view.

    Add a new view for printing:

    1. Open the Views task pane.
    2. Click Add A New View.
    3. Name the view Print View, and then click OK.

    Design the print view:

    1. Open the Controls task pane.
    2. Insert a Repeating Section into the view.
    3. Click inside the Section to set the insertion point.
    4. Choose Page Break from the Insert menu.
    5. Press Enter to move to the next line.
    6. Click Repeating Table in the Controls task pane, type 4 for the number of columns, and then click OK.
    7. Type Year, Make, Model and Description for the column header labels
    8. Right-Click the Year label, and then choose Table Properties.
    9. On the Column tab of the Repeating Table Properties dialog box, set the column widths of columns A-D to 56px, 75px, 91px and 400px respectively, as shown in Figure 2.

    Figure 2. The completed print view.

    As you inserted controls into the view, InfoPath automatically generated schema nodes for these controls. Let's rename these generated nodes so that they are easier to identify.

    Rename the generated schema nodes:

    1. Open the Data Source task pane.
    2. Choose Main from the Data Source drop down list.
    3. Double-click each node in the data source tree and rename as follows (refer to Figure 3):
      • group1 to Document
      • group2 to Page
      • group3 to Table
      • group4 to Row
      • field1 to Year
      • field2 to Make
      • field3 to Model
      • field4 to Description

    Figure 3. The revised schema.

    Now that we have finished designing our form layout, let's add functionality by adding code. The code will generate our print list when the user switches to the print view.

    Add the OnSwitchView event handler code:

    1. Choose Programming | On Switch Views Event from the Tools menu.
    2. In the Microsoft Script Editor, replace the contents of the OnAfterChange event handler with the following code:

    if(XDocument.View.Name == "Print View")

    This specifies to call a custom function each time the user switches to the print view. Now let's write the code to generate the repeating table header pages.

    1. Paste the following code directly below the OnSwitchView() function:

    function GeneratePrintList()
        // Number of lines we want on a page.
        var MAX_ROWS_ON_PAGE = 10;

        // Number of lines we have placed on current page.
        var iCurrentPageRowCount = 0;

        // Index of the current page we are adding to.
        var iCurrentPageIndex = 1;

        // Set up access to secondary data source.
        var domVehicles = XDocument.GetDOM("Vehicles");   

        // Source nodes.
        var oVehicles = domVehicles.selectNodes("/Vehicles/Vehicle");

        // Now we will get a node list for each of our groups and clear them
        // of all children. We will be generating the list from scratch each
        // time we switch to the Print View so that we can deal with changes
        // that have occurred since the last generation of the list.

        // ALL ROWS
        // Get the node list.
        var oRows = XDocument.DOM.selectNodes("/my:myFields/my:Document/my:Page/my:Table/my:Row");

        // Save a sample node from this list for later - we will have to clone
        // it to add our rows back in.
        var oSampleRow = oRows.nextNode();

        // Remove all the rows from all the repeating tables.

        // ALL TABLES
        var oTables = XDocument.DOM.selectNodes("/my:myFields/my:Document/my:Page/my:Table");   
        var oSampleTable = oTables.nextNode();   

        // ALL PAGES
        var oPages = XDocument.DOM.selectNodes("/my:myFields/my:Document/my:Page");   
        var oSamplePage = oPages.nextNode();   

        // THE DOCUMENT
        var oDocument = XDocument.DOM.selectSingleNode("/my:myFields/my:Document");   

        // We have only 1 document node, but we just emptied it of all
        // contents, so we must add back in at least one page and one
        // table node because we know we will be adding at least one row
        // into this table.

        // Add the first page.

        // Add the first table.

        // Loop through all source nodes and add them
        // to the table we are generating.
        while(oSrc = oVehicles.nextNode())
            // We are about to put a new row on the page.

            // If the row we about to put on the page will not fit, create a new page.
            if(iCurrentPageRowCount > MAX_ROWS_ON_PAGE)
                // Reset our row count (we are about to add a row to the next page,
                // so that page's row count will be 1).
                iCurrentPageRowCount = 1;

                // We are creating a new page and working with it, so increment the index.
                iCurrentPageIndex += 1;

                // Add the new page.
                var oNewPage = oDocument.appendChild(oSamplePage.cloneNode(false));

                // Add a new table to the page that we just created.

            // Create a new row by cloning our sample row.
            // This node must be cloned using "deep" mode to maintain structure of the
            // DOM below it. However, we are about to overwrite all of the nodes anyway.
            var oNewRow = oSampleRow.cloneNode(true);

            // Set the values in the new row from the source row

            // that we are reading from on this iteration.

            oNewRow.selectSingleNode("my:Year").text = oSrc.selectSingleNode("@year").nodeValue;

            oNewRow.selectSingleNode("my:Make").text = oSrc.selectSingleNode("@make").nodeValue;

            oNewRow.selectSingleNode("my:Model").text = oSrc.selectSingleNode("@model").nodeValue;

            oNewRow.selectSingleNode("my:Description").text = oSrc.selectSingleNode("@description").nodeValue;

            // Add the row to the table that is on the current page.
            XDocument.DOM.selectSingleNode("/my:myFields/my:Document/my:Page[" + iCurrentPageIndex + "]/my:Table").appendChild(oNewRow);

        } // End while looping through all source nodes.
    } // End function GeneratePrintList().

    Try it:

    1. Switch back to the InfoPath Designer.
    2. Open the Views task pane, and then select View 1.
    3. Preview the form.
    4. Choose Print View from the Views menu.
    5. Choose Print Preview from the File menu.

    While editing, the tables will appear to be right next to each other vertically, but when looking at the print preview, each page has a table with the column headers and ten data rows (as specified in our code). You will also notice that at the bottom of the first table, the row for "Jaguar XJ6" breaks in the middle, carries over to the next page, and is immediately followed by page break. This creates a huge gap in the printed pages.

    The first page is blank because the page break appears before the very first page node. This can easily be remedied by placing the page break inside a section, and then adding conditional formatting to hide the section for the first page node.


    To prevent rows from breaking across pages you need to take into consideration more than the just the number of rows per page; you need to know the number of lines of text per page. This process requires quite a bit of tweaking to determine the most precise method of counting lines per row and the number of lines that you can fit on a page. This will depend on the kind of data your rows contain and how it is formatted.

    To begin this process you will need to change the iCurrentPageRowCount variable to iCurrentPageLineCount, change the if-statement that controls whether to start a new page, and implement a method to count the number of lines of text in each row. We do this by tracking the number of lines that fit on a page and the number of characters per line before wrapping occurs.

    To determine the number of lines per page, view the form in print preview mode and count the number of lines (not rows) that fit on the first page. In this example, we know that the description field will always have more lines than any of the other fields, so we count each line in each description field on the first page to determine 48 lines per page. We will use 47 as our value, underestimating on the side of caution.

    Determining the number of characters that will fit on each line of the description field, before wrapping to a new line, is more of an estimate. A line that wraps to a new line must be counted as two lines. In this example, our description field is 400px wide, which allows approximately 58 characters before wrapping to the next line. To determine this number we look at the one wrapping line from all of the descriptions (the last line of the last description). We then count the number of characters on the line before it breaks, which is 58. Because the wrapping behavior depends on the length of the word at the end of the line, this number is only an estimate. Therefore, to be safe, we will use 55.

    function generatePrintList()
        // Number of lines we want on a page.
        var MAX_LINES_ON_PAGE = 47;

        // Number of lines we have placed on current page.
        var iCurrentPageLineCount = 0;

    . . .

        // Loop through all source nodes and add them
        // to the table we are generating.
        while(oSrc = oVehicles.nextNode())
            // We are about to put a new row on the page.
            var iLinesInRow = CountLinesInRow(oSrc);
            iCurrentPageLineCount += iLinesInRow;

            // If the row we about to put on the page
            // will not fit, create a new page.
            if(iCurrentPageLineCount > MAX_LINES_ON_PAGE)
                // Update the line count of the page we are about to add.
                iCurrentPageLineCount = iLinesInRow;

                // We are creating a new page and working with it,
                // so increment the index.
                iCurrentPageIndex += 1;

    . . .

    Now, let's implement the CountLinesInRow() function to accurately count the number of lines of text in each row added to the page. Paste the following code below the GeneratePrintList() function:

    function CountLinesInRow(oRow)
        // Select the field that is always vertically bigger than the other
        // fields in the row.
        var oDescription = oRow.selectSingleNode("@description ");
        var sText = oDescription.text;

        // 'Description' is formatted so that line breaks are allowed
        // and auto-wrapping occurs approximately every 55 characters   
        var iCharsPerLine = 55;

        // Lines counted so far
        var iNumLines = 0;

        // Split the field at the line breaks
        var arrLines = sText.split("\n");

        // We know that each element created from this split is at
        // least one line (hence Math.ceil()) and every 45 characters
        // is an additional line.

        for(i = 0; i < arrLines.length; i++)
            iNumLines += Math.ceil(arrLines[i].length / iCharsPerLine);

        return iNumLines;

    Try it:

    Preview your form as before and examine the print preview. You will notice that our code forces the Jaguar row to appear not on the first page but the second, preventing the row from splitting across pages.

    When performing your code tweaks, remember to err on the side of caution—it is better to overestimate the number of lines per row, and characters per line, than to underestimate. Underestimating will cause contents of the row to carry over onto the next page—exactly what you are trying to prevent.

Copyright © 2003-2019 Qdabra Software. All rights reserved.
View our Terms of Use.