Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Introducing PdfRport

5.00/5 (29 votes)
14 Feb 2013CPOL9 min read 100.7K   3.5K  
PdfReport is a code-first reporting engine which is built on top of the iTextSharp and EPPlus libraries.

Introduction

PdfReport is a code-first reporting engine, which is built on top of the iTextSharp and EPPlus libraries. It's compatible with both .NET 3.5+ Web and Windows applications. PdfReport supports a wide range of data sources from data tables to in-memory strongly typed lists without needing a database. It saves you time from searching and learning a lot of tips and tricks of iTextSharp and EPPlus libraries. It's designed to be compatible with RTL languages.

Image 1

How to start using PdfRport 

To create your first PdfReport:

  1. Create a new Class Library project in Visual Studio. We will use it as the base report classes container for both Windows and Web applications.
  2. Then add new references to the following assemblies: PdfReport, iTextSharp, and EPPlus. You can download them from http://pdfreport.codeplex.com/releases/

    Or just use the NuGet PowerShell console to add these references automatically:

    PM> Install-Package PdfReport

    http://nuget.org/packages/PdfReport/

  3. Add the following classes to the Class Library project:
  4. C#
    using System.Web;
    using System.Windows.Forms;
     
    namespace PdfReportSamples
    {
        public static class AppPath
        {
            public static string ApplicationPath
            {
                get
                {
                    if (isInWeb)
                        return HttpRuntime.AppDomainAppPath;
     
                    return Application.StartupPath;
                }
            }
     
            private static bool isInWeb
            {
                get
                {
                    return HttpContext.Current != null;
                }
            }
        }
    }

We will use this class to specify the location of the produced PDF file. It needs the following references as well:

  • System.Windows.Forms.dll
  • System.Web.dll
C#
using System;
 
namespace PdfReportSamples.IList
{
    public class User
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public string LastName { set; get; }
        public long Balance { set; get; }
        public DateTime RegisterDate { set; get; }
    }
}
 
//"User" class will be used for creating an in-memory generic list data source.
//And now add the main report class:

using System;
using System.Collections.Generic;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;
 
namespace PdfReportSamples.IList
{
    public class IListPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.LeftToRight);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", 
                  Application = "PdfRpt", Keywords = "IList Rpt.", 
                  Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\arial.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.RunDirection(PdfRunDirection.LeftToRight);
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("Our new rpt.");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.ClassicTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(5);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<User>();
                for (int i = 0; i < 200; i++)
                {
                    listOfRows.Add(new User { Id = i, LastName = "LastName " + i, 
                       Name = "Name " + i, Balance = i + 1000 });
                }
                dataSource.StronglyTypedList(listOfRows);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("Summary");
                summarySettings.PreviousPageSummarySettings("Previous Page Summary");
                summarySettings.PageSummarySettings("Page Summary");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("#");
                });
 
                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("Id");
                });
 
                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("Name");
                });
 
                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.LastName);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("Last Name");
                });
 
                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Balance);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("Balance");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });
 
                })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
                export.ToCsv();
                export.ToXml();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptIListSample.pdf"));
        }
    }
}

You can find its latest version here too.

To use this class and create a new PDF report file, we can write:

C#
var rpt = new IListPdfReport().CreatePdfReport();
// rpt.FileName
  • In the DocumentPreferences method we can specify the direction of the report (PdfRunDirection: RTL or LTR), page size (PdfPageSize), its orientation (PageOrientation), and so on.
  • Then it's necessary to determine the default report font files in the DefaultFonts method. The first font will be the main font and the second font will be used as the fallback font.
  • PdfReport comes with built-in footer and header samples. It's possible to customize these elements by implementing the IPageFooter and IPageHeader interfaces. We will discuss it in the other How-To's.
  • By using MainTableTemplate, we can define the main grid's template. There are some predefined templates available in the PdfReport library. Also it's possible to create new templates by implementing the ITableTemplate interface.
  • The MainTablePreferences method will be used for specifying the settings of the main report's grid, such as how many rows per page should be available (if we don't specify it, rows count will be calculated automatically based on the page size).
  • The ColumnsWidthsType method accepts four different values:

    1. Relative: Each column has a relative width equal to 1. Example: Relative values = 2, 1, 1. This means that you want to divide the width of the table into four parts (2 + 1 + 1): two parts for the first column, one part for columns two and three.
    2. Absolute: The absolute width expressed in user space units.
    3. EquallySized: Equally sized columns. In this case, all of the specified widths will be ignored.
    4. FitToContent: Tries to resize the columns automatically. In this case, all of the specified widths will be ignored.
  • The MainTableDataSource method sets the data source of the main grid. For instance, in the above example, the StronglyTypedList method will process the list of users. There are other built-in data source methods in the PdfReport library. For example, if you want to use raw SQL and work with the database directly, try the following methods:
C#
//SQL server data source
public void SqlDataReader(string connectionString, string sql, params object[] parametersValues)
 
//.mdb or .accdb files
public void AccessDataReader(string filePath, string password, string sql, params object[] parametersValues)
 
//Odbc data source
public void OdbcDataReader(string connectionString, string sql, params object[] parametersValues)
 
// A generic data reader data source
public void GenericDataReader(string providerName, string connectionString, string sql, params object[] parametersValues)

It's possible to write parametric queries in all of the above methods. These parameters should start with the @ symbol. Here is a quick sample which shows how to work with SQLite databases in PdfReport:

C#
dataSource.GenericDataReader(
    providerName: "System.Data.SQLite",
    connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
    sql: @"SELECT [url], [name], [NumberOfPosts], [AddDate]
               FROM [tblBlogs]
               WHERE [NumberOfPosts]>=@p1",
    parametersValues: new object[] { 10 }
);

Add a reference to the  System.Data.SQLite assembly and then use the above generic data reader. The same rule applies to MySQL and other databases.

  • The MainTableSummarySettings method determines the auto generated summary/total labels and their position to show. It's optional.
  • By using the optional MainTableColumns method, it's possible to determine the exact columns of the report's grid. Each column should be present in the data source. Also it's possible to define the calculated fields as well. It will be discussed in other How-To's later. In the MainTableColumns method, you can specify the related property of the column, its width, visibility, order, and so on. Here by using the ColumnItemsTemplate method, we can determine the type of the current field and how it should be displayed. If it should be displayed as a text, use the template.TextBlock() method (it's the default method). Also there are some other built-in cell templates such as image, hyperlinks, etc. It's possible to use custom column templates by implementing the IColumnItemsTemplate interface too.
    If you want to format the cell's value before rendering, use the template.DisplayFormatFormula method. It's a callback method, which gives you the actual value of the cell and then you can format it and return the final result to show on the report.
  • By using the column.AggregateFunction method, we can determine the related aggregate method of the current column. There are some predefined numeric aggregate functions available in the PdfReport library. Also it's possible to write custom ones by implementing the IAggregateFunc interface.
  • The MainTableEvents method provides access to the internal events of the main grid. For instance if the data source is empty, the DataSourceIsEmpty event will be raised.
  • Also it's possible to export the main table's data as Excel, CSV, XML, etc. files. All of these exported files will be embedded in the final PDF file automatically.

How to create auto generated/dynamic columns

Specifying the MainTableColumns method and all of its definitions is arbitrary in PdfReport. Just omit this part and then the final report will be created dynamically based on the available columns in the provided data source. This feature gives us great flexibility, but after some time we need to customize these kinds of reports: how to format DateTimes, how to add the total number of numeric fields and so on.

Here are some tips about customizing the auto generated columns:

a) Use aliases to specify the header cells:

If you are using the SQL based data sources such as GenericDataReader, to customize the header cells just define the column aliases in your final SQL:

SQL
SELECT [NumberOfPosts] as 'Number of posts'
FROM [tblBlogs]
WHERE [NumberOfPosts]>=@p1

b) Specifying the rendering conventions, based on the data types of columns

By using the MainTableAdHocColumnsConventions method, it's possible to alter the rendering conditions of the dynamic columns. In the MainTableAdHocColumnsConventions method, we can include the auto generated row column in the final report:

C#
adHocColumns.ShowRowNumberColumn(true);
adHocColumns.RowNumberColumnCaption("#");
Or it's possible to format a cell's value based on its type:
adHocColumns.AddTypeDisplayFormatFormula(
                     typeof(DateTime),
                     data => { return PersianDate.ToPersianDateTime((DateTime)data); }
                 );

Here we are altering the rendering value of all of the DateTime columns.

C#
adHocColumns.AddTypeAggregateFunction(
                     typeof(Int64),
                     new AggregateProvider(AggregateFunction.Sum)
                     {
                         DisplayFormatFormula = obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)
                     });

Or we can determine the specific AggregateFunction of the given data type.

c) Using data annotations to define column properties

If you are using a generic list data source, it's possible to omit the MainTableColumns method and all of its definitions by replacing them with data annotations:

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using PdfReportSamples.Models;
using PdfRpt.Aggregates.Numbers;
using PdfRpt.ColumnsItemsTemplates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.DataAnnotations;
 
namespace PdfReportSamples.DataAnnotations
{
    public class Person
    {
        [IsVisible(false)]
        public int Id { get; set; }
 
        [DisplayName("User name")]
        //Note: If you don't specify the ColumnItemsTemplate, a new TextBlockField() will be used automatically.
        [ColumnItemsTemplate(typeof(TextBlockField))]
        public string Name { get; set; }
 
        [DisplayName("Job title")]
        public JobTitle JobTitle { set; get; }
 
        [DisplayName("Date of birth")]
        [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
        public DateTime DateOfBirth { get; set; }
 
        [DisplayName("Date of death")]
        [DisplayFormat(NullDisplayText = "-", DataFormatString = "{0:MM/dd/yyyy}")]
        public DateTime? DateOfDeath { get; set; }
 
        [DisplayFormat(DataFormatString = "{0:n0}")]
        [CustomAggregateFunction(typeof(Sum))]
        public int Salary { get; set; }
 
        [IsCalculatedField(true)]
        [DisplayName("Calculated Field")]
        [DisplayFormat(DataFormatString = "{0:n0}")]
        [AggregateFunction(AggregateFunction.Sum)]
        public string CalculatedField { get; set; }
 
        [CalculatedFieldFormula("CalculatedField")]
        public static Func<IList<CellData>, object> CalculatedFieldFormula =
                                                    list =>
                                                    {
                                                        if (list == null) return string.Empty;
                                                        var salary = (int)list.GetValueOf<Person>(x => x.Salary);
                                                        return salary * 0.8;
                                                    };//Note: It's a static field, not a property.
    }
}
  • If you don't want to show a property in the final report, use the [IsVisible(false)] attribute.
  • DisplayName attribute will be used to define the header cells of the report.
  • Specifying the ColumnItemsTemplate attribute is optional and if it's not defined, TextBlockField will be used automatically.

Other predefined column cell templates are defined in the PdfRpt.ColumnsItemsTemplates namespace.

  • To format the displayed dates or numbers, use the DisplayFormat attribute.
  • To add the total rows, specify the CustomAggregateFunction attribute. There are some built-in aggregate functions in the  PdfRpt.Aggregates.Numbers namespace.
  • To define a calculated field/column, specify the [IsCalculatedField(true)] attribute. And then add a new static field (not a property) to define the related formula. This new field should be decorated with the CalculatedFieldFormula attribute to determine the property name of the calculated field.

Here are some full samples about auto generated columns:

How to define a calculated column

Some times we need to create a new column which is not present in the data source, based on the values of other columns. There are two types of calculated columns in PdfReport:

a) Row numbers columns

Just set column.IsRowNumber(true);. And then a new row number column which is not included in the data source will be available in the final report.

b) To define a custom calculated column, we need to specify its calculation formula by setting the column.CalculatedField method:

C#
columns.AddColumn(column =>
{
 column.PropertyName("CF1");
 column.CalculatedField(
     list =>
     {
         if (list == null) return string.Empty;
         var name = list.GetSafeStringValueOf<User>(x => x.Name);
         var lastName = list.GetSafeStringValueOf<User>(x => x.LastName);
         return name + " - " + lastName;
     });
 column.HeaderCell("Full name");
 column.Width(3);
 column.CellsHorizontalAlignment(HorizontalAlignment.Center);
 column.IsVisible(true);
 column.Order(4);
});

The column.CalculatedField method's argument gives us the list of values of the current row:

C#
public void CalculatedField(bool isCalculatedField, Func<IList<CellData>, object> calculatedFieldFormula)

Now we have time to build the current cell's value based on the other column values. There are some helper methods defined in the  PdfRpt.Core.Helper namespace such as GetSafeStringValueOf to help working with IList<CellData> data more easily. Defining a PropertyName is mandatory, but in this case it can be an arbitrary text.

Here is the full sample which shows how to define a calculated column: CalculatedFields

How to show images in PdfReport

Usually we have two kinds of images in reports:

a) Images loaded from the file system

To show these kind of images we need to change the default ColumnItemsTemplate which is TextBlock to ImageFilePath:

C#
columns.AddColumn(column =>
{
    column.PropertyName<ImageRecord>(x => x.ImagePath);
    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
    column.IsVisible(true);
    column.Order(2);
    column.Width(3);
    column.HeaderCell("Image");
    column.ColumnItemsTemplate(t => t.ImageFilePath(defaultImageFilePath: string.Empty, fitImages: false));
});

Here defaultImageFilePath is the default image path in the case of missing images.

b) Images stored in databases

Showing images stored as binary data in databases is similar to (a). We just need to use the suitable ColumnItemsTemplate which is the ByteArrayImage template:

C#
columns.AddColumn(column =>
{
 column.PropertyName("thumbnail");
 column.CellsHorizontalAlignment(HorizontalAlignment.Center);
 column.IsVisible(true);
 column.Order(5);
 column.HeaderCell("Image");
 column.ColumnItemsTemplate(t => t.ByteArrayImage(defaultImageFilePath: string.Empty, fitImages: false));
});

Here you can find the complete samples of (a) and (b):

Conditional formatting in PdfReport

Suppose we want to display the list of personnel in a year. In this report each month cell with month=7 should be displayed with a different color. To achieve this goal we can use the template.ConditionalFormatFormula method:

C#
columns.AddColumn(column =>
{
 column.PropertyName("Month");
 column.CellsHorizontalAlignment(HorizontalAlignment.Center);
 column.IsVisible(true);
 column.Order(2);
 column.Width(2);
 column.HeaderCell("Month");
 column.ColumnItemsTemplate(template =>
 {
     template.TextBlock();
     template.ConditionalFormatFormula(list =>
     {
         var cellValue = int.Parse(list.GetSafeStringValueOf("Month", nullValue: "0"));
         if (cellValue == 7)
         {
             return new CellBasicProperties
             {
                 PdfFontStyle = DocumentFontStyle.Bold | DocumentFontStyle.Underline,
                 FontColor = new BaseColor(System.Drawing.Color.Brown),
                 BackgroundColor = new BaseColor(System.Drawing.Color.Yellow)
             };
         }
         return new CellBasicProperties { PdfFontStyle = DocumentFontStyle.Normal };
     });
 });
});

template.ConditionalFormatFormula is a callback method which gives us the list of the current row's data. Now based on the value of the month (or other properties) we can return the "new CellBasicProperties" with a different font color, style, etc. You can find the related sample here.

How to create custom main table's templates

There are some built-in report templates in the PdfReport library such as BasicTemplate.RainyDayTemplate, BasicTemplate.SilverTemplate, etc. Also it's possible to create the new main table's templates by implementing the ITableTemplate interface. For instance:

C#
using System.Collections.Generic;
using System.Drawing;
using iTextSharp.text;
using PdfRpt.Core.Contracts;
 
namespace PdfReportSamples.HexDump
{
    public class GrayTemplate : ITableTemplate
    {
        public HorizontalAlignment HeaderHorizontalAlignment
        {
            get { return HorizontalAlignment.Center; }
        }
 
        public BaseColor AlternatingRowBackgroundColor
        {
            get { return new BaseColor(Color.WhiteSmoke); }
        }
 
        public BaseColor CellBorderColor
        {
            get { return new BaseColor(Color.LightGray); }
        }
 
        public IList<BaseColor> HeaderBackgroundColor
        {
            get { return new List<BaseColor> { new BaseColor(
              ColorTranslator.FromHtml("#990000")), 
              new BaseColor(ColorTranslator.FromHtml("#e80000")) }; }
        }
 
        public BaseColor RowBackgroundColor
        {
            get { return null; }
        }
 
        public IList<BaseColor> PreviousPageSummaryRowBackgroundColor
        {
            get { return new List<BaseColor> { new BaseColor(Color.LightSkyBlue) }; }
        }
 
        public IList<BaseColor> SummaryRowBackgroundColor
        {
            get { return new List<BaseColor> { new BaseColor(Color.LightSteelBlue) }; }
        }
 
        public IList<BaseColor> PageSummaryRowBackgroundColor
        {
            get { return new List<BaseColor> { new BaseColor(Color.Yellow) }; }
        }
 
        public BaseColor AlternatingRowFontColor
        {
            get { return new BaseColor(ColorTranslator.FromHtml("#333333")); }
        }
 
        public BaseColor HeaderFontColor
        {
            get { return new BaseColor(Color.White); }
        }
 
        public BaseColor RowFontColor
        {
            get { return new BaseColor(ColorTranslator.FromHtml("#333333")); }
        }
 
        public BaseColor PreviousPageSummaryRowFontColor
        {
            get { return new BaseColor(Color.Black); }
        }
 
        public BaseColor SummaryRowFontColor
        {
            get { return new BaseColor(Color.Black); }
        }
 
        public BaseColor PageSummaryRowFontColor
        {
            get { return new BaseColor(Color.Black); }
        }
 
        public bool ShowGridLines
        {
            get { return true; }
        }
    }
}

To use this new template, we can write:

C#
.MainTableTemplate(template =>
{
      template.CustomTemplate(new GrayTemplate());
})

Some tips and tricks:

  • Colors in the iTextSharp library are defined by the BaseColor class. If you want to convert the colors of System.Drawing to BaseColor, just pass it to the BaseColor's constructor:
  • C#
    var blackColor = new BaseColor(Color.Black);
  • There are some useful helper methods in .NET classes such as ColorTranslator.FromHtml to convert and use the HTML colors.
  • If you want to define a transparent color here, just return null.
  • In the ITableTemplate interface, some colors are defined as a list. These properties can accept one or max two colors. If two colors are specified, an automatic gradient of these colors will be shown on the report.

How to use HTML to create custom cell templates

Suppose we have a list of users with their names and photos. We want to show the name and photo of each user in one cell and not two separate cells. The built-in cell templates of PdfReport are suitable for displaying only one object per cell. To define a custom cell template, we need to implement the IColumnItemsTemplate interface or use a shortcut:
C#
columns.AddColumn(column =>
{
 column.PropertyName("User");
 column.CellsHorizontalAlignment(HorizontalAlignment.Center);
 column.IsVisible(true);
 column.Order(1);
 column.Width(3);
 column.HeaderCell("User");
 column.CalculatedField(list =>
     {
         var user = list.GetSafeStringValueOf("User");
         var photo = new Uri(list.GetSafeStringValueOf("Photo"));
         var image = string.Format("<img src='{0}' />", photo);
         return
                @"<table style='width: 100%; font-size:9pt;'>
                            <tr>
                                <td>" + user + @"</td>
                            </tr>
                            <tr>
                                <td>" + image + @"</td>
                            </tr>
                   </table>
                 ";
     });
 column.ColumnItemsTemplate(template =>
     {
         template.Html(); // Using iTextSharp's limited HTML to PDF capabilities (HTMLWorker class).
     });
});

Here iTextSharp's HTMLWorker class is used behind the scenes. By using CalculatedField, we can inject our new value of a cell and then process it by the selected ColumnItemsTemplate. Note: HTML to PDF capabilities of  iTextSharp's HTMLWorker class are very limited, and don't expect too much about it. You can find this sample here.

More guides and samples

PdfReport comes with more than 40 samples. Please download its source code from CodePlex and check out its samples folder.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)