kuujinbo_dot_info

Posted 2008-07.

iTextSharp makes things a lot easier for you. We're going to create a HttpHandler that generates a simple PDF report from a database query with about 100 lines of code.

Formatting the Report

For simplicity the example limits the report options. However, it wouldn't take much imagination to extend the example, and add additional class members initialized from a XML file or database to generate (in the UI) a selectable list of reports each with it's own settings (the former is what this example is based upon).

A Simple Helper Class

First we create a helper class that allows some simple report features:

  • Report name displayed on the first page.
  • Table's column widths
  • Font size of the tabular data.
  • Print orientation; portrait (default) or landscape.
public class report_format {
  public string report_name, csv_width;
  public bool landscape;
// you will **NOT** hard-code report headings in real life...
  public readonly string[] headings =
    {"Country", "IP Block Start", "IP Block End"};
// instead something like this...
  public int[] column_widths() {
    // **AND** verify tmp.Length == headings.Length
    string[] tmp = Regex.Split(csv_width, ",");
    int[] cw = new int[tmp.Length];
    for (int i = 0; i < tmp.Length; ++i) {
      cw[i] = Convert.ToInt32(tmp[i]);
      if (cw[i] > 4 || cw[i] < 1) cw[i] = 8;
    }
    return cw;
  }
  private int _font_size;
  public int font_size{
    get { return _font_size; }
    set { _font_size = value > 6 && value < 10 ? value : 8; }
  }
}

As noted in the comments, in a real-world application you would at a minumum:

  • Allow configurable, not hard-code the report headings.
  • Include properties with private members instead of marking everything public.
  • Depending on where the configurable options are coming from, include appropriate sanity-checks.

Note that we pass a comma-separated string of integer values to column_widths(); more on that later.

HttpHandler

It's assumed you know the minimum requirement - implement IHttpHandler, and define: (a) IsReusable, and (b) ProcessRequest(). Simple:

public class MY_CLASS_NAME : IHttpHandler {
// ----------------------------------------
  // helper class above
  private report_format _rf;
  // configurable tabular data font
  private Font _report_font;
  // iText PDF document object
  private Document _document;
  // iText table object; filled by database query 
  private PdfPTable _pdf_table;
  // iText cell in PdfPTable
  private PdfPCell _data_cell;
// ----------------------------------------
  public void ProcessRequest(HttpContext context) {
    HttpRequest hc = context.Request;
// sanity-checks omitted for brevity
    string rname = hc.QueryString[0],
      rfont  = hc.QueryString[1],
      rwidth = hc.QueryString[2],
      rlands = hc.QueryString[3];

    _rf = new report_format();
    _rf.report_name = rname;
    _rf.csv_width   = rwidth;
    int tmp_fs;
    _rf.font_size = Int32.TryParse(rfont,out tmp_fs) ? tmp_fs : 8;
    bool tmp_landsc;
    _rf.landscape = rlands != "1" ? false : true;
    _write_pdf();
  }
// ----------------------------------------
  public bool IsReusable { get { return false; } }
}

Nothing really notable here. All we're doing is getting options from the querystring and setting our helper class member properties for the PDF report.

PdfPTable

Does the bulk of the work to generate a professional report:

private PdfPTable _init_pdfptable() {
  _pdf_table = new PdfPTable(_rf.headings.Length);
  PdfPCell hc = new PdfPCell();
  // the report's column headings  
  foreach (string s in _rf.headings) {
    hc = new PdfPCell(new Paragraph(s, _report_font));
    // header row background color
    hc.GrayFill = 0.7F;
    _pdf_table.AddCell(hc);
  }
  _pdf_table.SetWidths(_rf.column_widths());
  _pdf_table.WidthPercentage = 100;
  _pdf_table.HeaderRows = 1;
  return _pdf_table;
}
  • When instantiating a PdfPTable object you must pass the number of columns the table needs. After that iText takes care of adding rows to the table, but you're responsible for adding the number of cells specified when you called the constructor.
  • We pass an array of integers to PdfPTable.SetWidths(). Each value in the array is a relative width; iText calculates each column's absolute width based upon the available width on the current page.
  • If you don't explicitly set the PdfPTable.WidthPercentage property, the table's default width is 80% of the page.
  • For simplicity, we're adding PdfPCell objects to the table in text mode, which means that settings in each Paragraph object are trumped by PdfPCell; specifically alignment, indentation, and leading spacing. If you want to control properties of each cell, see PdfPCell.AddElement(IElement element)
  • Should be self-explanatory, but PdfPTable.HeaderRows() determines the number of table header rows displayed at the top of each page.

Filling the Table with Results from the Database Query

Is the easy part, pick your favorite DBMS.

private void _do_sql() {
// ########################################
// specific database implementation omitted
// ########################################
  using (DbDataReader r = cmd.ExecuteReader()) {
    int row_count = 0;
    while (r.Read()) {
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// clear table contents every _MAX_ROWS, otherwise PdfPTable
// object potentially may use **LARGE** amount of memory
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
      if (++row_count % _MAX_ROWS == 0) {
        // add table to Document
        _document.Add(_pdf_table);
        // delete _MAX_ROWS from table to free memory
        _pdf_table.DeleteBodyRows();
        // let iText manage when table header written
        _pdf_table.SkipFirstHeader = true;
      }
      for (int i = 0; i < r.FieldCount; ++i) {
        _data_cell = new PdfPCell(
          new Paragraph(r[i].ToString(), _report_font)
        );
        _pdf_table.AddCell(_data_cell);
      }
    }
  }
  _document.Add(_pdf_table);
}

Take note of the block of code where we're checking how many rows have been added to the table. The data (table cells with results from the database query) has to be stored somewhere; that somewhere is the PdfPTable object. Depending on your hardware and how large the table is you may run into some serious overhead. To offset the risk we do the following:

  • Set a managable upper limit, _MAX_ROWS, of rows that can be added to the table.
  • When that limit is reached:
    1. Add the partially completed PdfPTable object to the Document object; Document.Add().
    2. Delete the rows added above; Document.DeleteBodyRows()
    3. Make sure the table's header is only added to a new page; Document.SkipFirstHeader= true;

In effect we're explicitly creating/monitoring our own buffer and flushing data at our set limit so we don't have to worry about the application becoming a resource hog. The rest of the code should be self-explanatory by looking at the method names; we're iterating though each row of the database query and adding the values to cells in the PdfPTable.

Wrapping up

All that's left is to stream the PDF to the client:

private void _write_pdf() {
  HttpResponse rs = HttpContext.Current.Response;
  rs.ContentType = "application/pdf";
  _document = new Document();
  try {
    PdfWriter writer = PdfWriter.GetInstance(_document, rs.OutputStream);
// set landscape **BEFORE** Open(), or first page will 
// have portrait orientation
    if (_rf.landscape ) _document.SetPageSize(new Rectangle(792, 612));
    _document.Open();
    Paragraph h1 = new Paragraph(_rf.report_name);
    h1.SpacingAfter = 8;
    _document.Add(h1);
    _report_font = new Font(Font.FontFamily.HELVETICA, _rf.font_size);
    _pdf_table = _init_pdfptable();
    _do_sql();
  } catch { throw; }
  finally { if (_document != null && _document.IsOpen()) _document.Close(); }
}

Notable

  • PDF page settings like the page orientation and margins must must be set before calling Document.Open(), or the setting(s) will not be applied to the first page.
  • You could set landscape orientation in the Document object's constructor;
    new Document(PageSize.LETTER.rotate());.
    Printed there's no difference, but if you need to manipulate the documnent and reference exact positions, you're better off explicitly calling Document.SetPageSize(Rectangle rectangle) - but that's an entirely different topic...

Check out the demo to see how everything turns out. Again, with a little thought you can easily extend it to include many more features. Although done in the demo, you're probably not going to set everything with parameters from the URL. And see this example to get an idea of how to add a header/footer to each page of your PDF report.

Report Heading:
Font Size:
Column widths:
Landscape: