Introducing PdfRport
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.

How to start using PdfRport
To create your first PdfReport:
- 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.
- 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
- Add the following classes to the Class Library project:
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
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:
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
andIPageHeader
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 theITableTemplate
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). 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.Abso
lute: The absolute width expressed in user space units.EquallySized
: Equally sized columns. In this case, all of the specified widths will be ignored.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, theStronglyTypedList
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:
The ColumnsWidthsType
method accepts four different values:
//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:
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 theMainTableColumns
method, you can specify the related property of the column, its width, visibility, order, and so on. Here by using theColumnItemsTemplate
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 thetemplate.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 theIColumnItemsTemplate
interface too.
If you want to format the cell's value before rendering, use thetemplate.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 theIAggregateFunc
interface. - The
MainTableEvents
method provides access to the internal events of the main grid. For instance if the data source is empty, theDataSourceIsEmpty
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:
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:
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.
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:
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 thePdfRpt.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 theCalculatedFieldFormula
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:
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:
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
:
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:
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:
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:
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:
.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 ofSystem.Drawing
toBaseColor
, just pass it to theBaseColor
's constructor:
var blackColor = new BaseColor(Color.Black);
ColorTranslator.FromHtml
to convert and use the HTML colors.null
.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 theIColumnItemsTemplate
interface or use a shortcut:
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.