Creating Multi-Page Spread Tables

Bekzat Karayev
April 20, 2021

When I first started creating PDF documents with PDFFlow I was surprised how easy and fast I was able to build PDF documents even with complex design and layouting of sections. I really like the intuitive naming of classes and methods, as well as a rich variety of options for formatting of the page elements that PDFFlow offers.

One of the features that impressed me the most is adding a table to a PDF document that spans multiple pages. This is a very common scenario for documents used in accounting, finance, education, commerce and retail. For example, quite often log books and warehouse inventories are presented in this format.

Suppose we need to create a 4 page car sales logbook to look as follows:

Getting a table to look like this, spanning multiple pages is rather challenging, despite it looking simple. In the rest of the article I will show you how to create this document using PDFFlow.

Getting started

First, let's create a new .NET Core console app "MyLogBook". I use Microsoft Visual Studio 2019, other versions should work just fine.
Let's add Gehtsoft.PDFFlowlib library to our project. You can download it as a nuget package from NuGet Gallery | Gehtsoft.PDFFlowLib.

Or directly from Visual Studio's Nuget Package Manager as shown below:

Nuget Package Manager

Type the library name and install the latest version. Agree to the changes of the solution file and accept terms of licensing. Now, we can use the PDFFlow classes and methods in our project.

Let’s start building our document by creating an instance of a … you guessed it … DocumentBuilder class. I told you already that this library’s component names are pretty straightforward and intuitive, didn’t I?)


using Gehtsoft.PDFFlow.Builder;

namespace MyLogBook
{
    class Program
    {
        static void Main(string[] args)
        {
            DocumentBuilder builder = DocumentBuilder.New();
        }
    }
}

Each document consists of sections. A section may contain a single page or many pages. In our case, the document will have only one section. This section will contain all pages of our document.

I should mention here that for most documents generated using PDFFlow, adding one section is enough. An example for a document that may require more than one section is a book with multiple chapters. It may have chapter names written at the top of each page. In that case, we need to add sections to the document for each chapter of the book and configure the headers of the sections separately. Another reason to have separate sections is when you have different page sizes or orientation in the document, for example, the first section is in landscape format, and the rest are in portrait mode. Yet another reason is when you need different page numeration or page footers/headers for each different part of the document.

Okay, back to our logbook. We can define page size, orientation and margins right away, as well as the default font for the text using methods of the SectionBuilder class:


SectionBuilder section = builder.AddSection();
    section
        .SetSize(PaperSize.A4)
        .SetOrientation(PageOrientation.Landscape)
        .SetMargins(50)
        .SetStyleFont(Fonts.Courier(20));

As you can see, our pages will be A4 size, landscape oriented, and all page margins are set to 50 points. Also, all the text in our document will be written in Courier, size 20, unless otherwise specified in the sub elements of the section.

Creating a two page spread table

Now let’s add a table, nothing difficult at all, just create an instance of a TableBuilder class:


    TableBuilder table = section.AddTable();

PDFFlow has several special methods that can spread this table into multiple pages. You can use the SetMultiPageSpread method and define how many columns of the table should be on the first page, how many on the second page and so on. But for the simplicity sake, we will let the library do it for us and call the SetAutomaticMultipageSpreadMode method:


    table
        .SetAutomaticMultipageSpreadMode()

Our next step is to set the number of columns of the table; we will add them one by one. AddColumnPercentToTable() method of the library allows you to specify the width of the column as a percentage of the page width. We can write the column header here as well:


        .AddColumnPercentToTable("#", 5)
        .AddColumnPercentToTable("Manufacturer", 25)
        .AddColumnPercentToTable("Model", 15)
        .AddColumnPercentToTable("VIN", 30)
        .AddColumnPercentToTable("Date Received", 15)
        .AddColumnPercentToTable("Source", 15)
        .AddColumnPercentToTable("#", 5)
        .AddColumnPercentToTable("Date Sent", 15)
        .AddColumnPercentToTable("Buyer Name", 30)
        .AddColumnPercentToTable("Buyer Address", 50);

Column headers will be written in the top row of the table. PDFFlow writes them in bold by default. Values for column widths are set arbitrarily, but since they are defined as percentages, you might want to check that the total width of the columns that you want to place on the same page equals 100. Otherwise, you will end up with too much white space on the right side of the pages of your document.

As you can see from the code above, our table will have 10 columns in total. Six of them, namely those with headers "#", "Manufacturer", "Model", "VIN", "Date Received", "Source" will appear on the first page, since their widths are set to 5, 25, 15, 25, 15 and 15 respectively, which add up to 100 percent. So this guarantees that the first page will not have any extra space horizontally, except for the margins. Other four columns will be rendered on the second page.

Adding data to the table

Our table is now configured, next we need to populate it with data. This data can be “hardcoded” right into the project’s source code, or may be parsed from any external data source. For this example, I will use a JSON file with the following content:


[
  { "Source": "Photobug", "Manufacturer": "Ford", "Model": "Explorer Sport","VIN": "WAUUL98E16A070717", "Received": "6/26/2018", "Sent": "6/7/2019","Buyer_Name": "Sancho McGorley", "Buyer_Address": "291 Leroy Crossing, VA, 23208"},
  { "Source": "Skiptube", "Manufacturer": "Mazda", "Model": "MX-5", "VIN": "1FTNF2A51AE584369", "Received": "12/25/2018", "Sent": null, "Buyer_Name": null, "Buyer_Address": null},
  { "Source": "Jaxworks", "Manufacturer": "Toyota", "Model": "Celica", "VIN": "3C3CFFAR7CT923454", "Received": "3/19/2019", "Sent": "11/30/2018", "Buyer_Name": "Alessandro Marxsen", "Buyer_Address": "0344 Monica Junction, CT, 06105"},
  { "Source": "Jabberstorm", "Manufacturer": "Jeep", "Model": "Cherokee", "VIN": "JHMZF1C69DS501747", "Received": "1/12/2019", "Sent": "11/9/2018", "Buyer_Name": "Ramsay Trayhorn", "Buyer_Address": "75480 Green Way, MO, 63131"},
  { "Source": "Brainlounge", "Manufacturer": "Mercedes-Benz", "Model": "CLK-Class", "VIN": "JTDZN3EU8FJ175489", "Received": "12/17/2018", "Sent": "5/16/2019", "Buyer_Name": "Shanan Oleksiak", "Buyer_Address": "8105 Nevada Trail, OK, 74126"},
  { "Source": "InnoZ", "Manufacturer": "Dodge", "Model": "Sprinter", "VIN": "JTDZN3EU8E3185961", "Received": "10/19/2018", "Sent": "5/8/2019", "Buyer_Name": "Micki Pendry", "Buyer_Address": "952 Twin Pines Crossing, IL, 62764"},
  { "Source": "Devshare", "Manufacturer": "Jeep", "Model": "Patriot", "VIN": "5GAKVCED5CJ544937", "Received": "10/3/2018", "Sent": "6/8/2019", "Buyer_Name": "Janela Handasyde", "Buyer_Address": "1 Menomonie Avenue, MN, 55557"},
  { "Source": "Tagchat", "Manufacturer": "Mitsubishi", "Model": "Diamante", "VIN": "JTHBP5C26B5203428", "Received": "7/20/2018", "Sent": "9/12/2018", "Buyer_Name": "Radcliffe Standrin", "Buyer_Address": "48407 Beilfuss Drive, CA, 91186"},
  { "Source": "Yombu", "Manufacturer": "Dodge", "Model": "Caravan", "VIN": "1N4AL3APXEC099673", "Received": "1/29/2019", "Sent": null, "Buyer_Name": null, "Buyer_Address": null},
  { "Source": "Feedfire", "Manufacturer": "Volvo", "Model": "V70", "VIN": "JM3KE2BE2E0511643", "Received": "3/22/2019", "Sent": "2/10/2019", "Buyer_Name": "Lin Levinge", "Buyer_Address": "87900 Norway Maple Junction, NY, 11054"},
  { "Source": "Wordpedia", "Manufacturer": "Honda", "Model": "Insight", "VIN": "1G6DL8E36D0041686", "Received": "1/30/2019", "Sent": "2/8/2019", "Buyer_Name": "Gayler Offa", "Buyer_Address": "802 Sunbrook Hill, NC, 28272"},
  { "Source": "Abatz", "Manufacturer": "Dodge", "Model": "Grand Caravan", "VIN": "2HNYD18243H477043", "Received": "5/28/2019", "Sent": null, "Buyer_Name": null, "Buyer_Address": null},
  { "Source": "Photobug", "Manufacturer": "Ford", "Model": "Explorer Sport", "VIN": "WAUUL98E16A070717", "Received": "6/26/2018", "Sent": "6/7/2019", "Buyer_Name": "Sancho McGorley", "Buyer_Address": "291 Leroy Crossing, VA, 23208"},
  { "Source": "Skiptube", "Manufacturer": "Mazda", "Model": "MX-5", "VIN": "1FTNF2A51AE584369", "Received": "12/25/2018", "Sent": null, "Buyer_Name": null, "Buyer_Address": null},
  { "Source": "Jaxworks", "Manufacturer": "Toyota", "Model": "Celica", "VIN": "3C3CFFAR7CT923454", "Received": "3/19/2019", "Sent": "11/30/2018", "Buyer_Name": "Alessandro Marxsen", "Buyer_Address": "0344 Monica Junction, CT, 06105"},
  { "Source": "Jabberstorm", "Manufacturer": "Jeep", "Model": "Cherokee", "VIN": "JHMZF1C69DS501747", "Received": "1/12/2019", "Sent": "11/9/2018", "Buyer_Name": "Ramsay Trayhorn", "Buyer_Address": "75480 Green Way, MO, 63131"},
  { "Source": "Brainlounge", "Manufacturer": "Mercedes-Benz", "Model": "CLK-Class", "VIN": "JTDZN3EU8FJ175489", "Received": "12/17/2018", "Sent": "5/16/2019", "Buyer_Name": "Shanan Oleksiak", "Buyer_Address": "8105 Nevada Trail, OK, 74126"},
  { "Source": "InnoZ", "Manufacturer": "Dodge", "Model": "Sprinter", "VIN": "JTDZN3EU8E3185961", "Received": "10/19/2018", "Sent": "5/8/2019", "Buyer_Name": "Micki Pendry", "Buyer_Address": "952 Twin Pines Crossing, IL, 62764"},
  { "Source": "Devshare", "Manufacturer": "Jeep", "Model": "Patriot", "VIN": "5GAKVCED5CJ544937", "Received": "10/3/2018", "Sent": "6/8/2019", "Buyer_Name": "Janela Handasyde", "Buyer_Address": "1 Menomonie Avenue, MN, 55557"},
  { "Source": "Tagchat", "Manufacturer": "Mitsubishi", "Model": "Diamante", "VIN": "JTHBP5C26B5203428", "Received": "7/20/2018", "Sent": "9/12/2018", "Buyer_Name": "Radcliffe Standrin", "Buyer_Address": "48407 Beilfuss Drive, CA, 91186"},
  { "Source": "Yombu", "Manufacturer": "Dodge", "Model": "Caravan", "VIN": "1N4AL3APXEC099673", "Received": "1/29/2019", "Sent": null, "Buyer_Name": null, "Buyer_Address": null},
  { "Source": "Feedfire", "Manufacturer": "Volvo", "Model": "V70", "VIN": "JM3KE2BE2E0511643", "Received": "3/22/2019", "Sent": "2/10/2019", "Buyer_Name": "Lin Levinge", "Buyer_Address": "87900 Norway Maple Junction, NY, 11054"},
  { "Source": "Wordpedia", "Manufacturer": "Honda", "Model": "Insight", "VIN": "1G6DL8E36D0041686", "Received": "1/30/2019", "Sent": "2/8/2019", "Buyer_Name": "Gayler Offa", "Buyer_Address": "802 Sunbrook Hill, NC, 28272"},
  { "Source": "Abatz", "Manufacturer": "Dodge", "Model": "Grand Caravan", "VIN": "2HNYD18243H477043", "Received": "5/28/2019", "Sent": null, "Buyer_Name": null, "Buyer_Address": null}
]

Let’s name this JSON file logbookdata.json and place it into the ..\bin\Debug\netcoreapp3.1\Content directory. FYI, all data in this file are fictitious, all matches with real people and events are purely coincidental! :)

Okay, now in order to use the data in C# code we need a model. Create a Model folder inside the project’s directory and add the LogBookModel class which is defined as follows:


using System;

namespace MyLogBook.Model
{
    public class LogBookModel
    {
        public string Source { get; set; }
        public string Manufacturer { get; set; }
        public string Model { get; set; }
        public string VIN { get; set; }
        public DateTime? Received { get; set; }
        public DateTime? Sent { get; set; }
        public string Buyer_Name { get; set; }
        public string Buyer_Address { get; set; }
    }
}

Now we need to convert the data, or, in other words, deserialize from JSON. For this purpose I use the Newtonsoft.Json framework for .NET. It can be added to the project with the Nuget Package Manager, as I showed before. After that, create a collection salesData inside our Main method and don't forget to insert the necessary namespaces:


    List salesData = JsonConvert
                                .DeserializeObject
                                (File.ReadAllText(Path.Combine
                                ("Content", "logbookdata.json")));

Let’s get back to the PDFFlow methods as we need to create the rows in our table and fill each cell of the table with content. Add the following logic to iterate through the salesData list and fill the table with data row by row:


   int index = 0;

    foreach (var item in salesData)
    {
        index += 1;
        string dateReceived = item.Received?.ToString
                            ("d/M/yyyy", CultureInfo
                            .CreateSpecificCulture("en-US"));
        string dateSent = item.Sent?.ToString
                            ("d/M/yyyy", CultureInfo
                            .CreateSpecificCulture("en-US"));

        table
            .AddRow()
            .SetFontSize(15)
            .AddCellToRow(index.ToString())
            .AddCellToRow(item.Manufacturer)
            .AddCellToRow(item.Model)
            .AddCellToRow(item.VIN)
            .AddCellToRow(dateReceived)
            .AddCellToRow(item.Source)
            .AddCellToRow(index.ToString())
            .AddCellToRow(dateSent)
            .AddCellToRow(item.Buyer_Name)
            .AddCellToRow(item.Buyer_Address);
     }

I will explain what we did here.
First of all, our table has "#" columns, where the order number is written. It should definitely help the readers of the document to follow the details of a specific sale on different pages. That’s why a variable index is introduced -- it stores the order of rows, and its value is incremented by one each time a new row with data is added to the table.

Secondly, the foreach loop enumerates through the salesData collection and every iteration creates a new row with the PDFFlow’s AddRow method. However, this method does not complete the row. In order to complete it, the AddCell method needs to be called as many times as we have columns in our table. We can specify the content of each cell by passing it as a string parameter to the AddCell method.

Generating a PDF document

Our last step is to form the document as a PDF file.
For this, just insert


    builder.Build("LogBook.pdf");

t the end of the Main method and run the project. After it is built successfully, the LogBook.pdf file should appear in the ../bin/Debug/netcoreapp3.1 directory. You can open it with any PDF viewer to see the result, and, voila, your multi-page spread table is ready.

Notice that we didn’t have to worry about whether the contents of the table would fit into a page or not. PDFFlow automatically splits the table into pages and carries the excess rows and columns to the next page. That’s a pretty cool feature, that can save you hours of coding time!).

You can find more examples of real-world documents generated using PDFFlow here on github: Real Document Examples.
They are also provided with extensive articles on how to build them.

If you want to know more about the PDFFlow library, check out GS PDFFlow website.

We believe that the PDFFlow library is the best and most affordable tool on the market to generate your business docs, whether you need to create а simple invoice, or a set of very complex regulatory documents.
If you are ready to get started or if you have any questions, please don’t hesitate to let us know your needs and learn more about our flat-rate custom development offer.
Bekzat Karayev
Developer in PDFFlow team
Currently, I work on PDFFlow, a cross-platform library that generates PDF documents from C# code. One of my responsibilities is to create examples of real-life documents for the users using this library.
Sign up to receive a monthly email on the latest news!
We will never spam you, transfer or sell your email.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Previous posts