Tuesday, November 3, 2015

Create a PDF file from a DataSet/DataTable and present that to the browser for preview with iTextSharp

I recently had a friend ask how he could create a PDF report and present that PDF to the user for preview prior to saving from a Windows Application.  It got a little tricky as you have to ensure that there is an application on the client machine that can read the PDF file.

Here we will create a PDF from an ASP.Net application and using the browser, present that PDF to the user for preview prior to saving using a memoryStream. 

In order to start our project, we need to define some prerequisites.  Of course we need to have some data to populate a table or grid on the PDF.  In this case, we will use the three-tier method (DAL, BLL, Presentation).  The code to generate the report will reside in the BLL above the presentation layer.  Finally, we need a trigger that will “activate” our code so that we know to begin creating the report.

As with all projects that include any kind of data, you start at the data.  How can you design an application for data if you don’t know what data to expect or what it looks like (types… etc). 

So let’s get started…

I am using VS2013 Professional, however this project is Framework dependent and should not be affected by your VS version.  For this project, you need to at least be at .NET 3.5 Framework.

Fire up VS and click File | New Web Site.  In the modal window that pops up, click ASP.NET Web Forms Site.  I save mine to the file system and the location is irrelevant in the context of this project.

image

Once you click ok, you should be taken to the website in the editor.  The first thing we need to do is create our three tiers to make our site readable and modular. 

Right-Click on the name of your solution | Highlight Add | Add ASP.NET Folder | Left-Click App_Code.  This will put the ASP.NET reserved folder App_Code in your solution. 

image

Right-Click the App_Code folder | Add | New Folder.  Name this folder BLL.  Repeat this step and create a folder named DAL.  Your solution Explorer should look like -

image

Now we need to create our classes for the PDF report.  Right-Click the DAL folder | Add | Class.  Name this class PDFExport.  Repeat this step under the BLL and name the class PDFExport.  So you should have a class named PDFExport under both the BLL and DAL folders.  You should be seeing an error (little red wavy line under the DAL folder and our new class – This is to be expected).

image

Why do we get the error?  This is because of namespacing.  There are two objects with the exact same name that exists in the same namespace.  So we need to separate these two objects in the hierarchy.  Double Left click the DAL/PDFExport.vb file (or the PDFExport.vb file located in the DAL folder).  You should see the following code:

Imports Microsoft.VisualBasic

Public Class PDFExport

End Class 

In the concept of namespacing, we can name the namespace anything we want as long as we know how to reference the namespace or class, otherwise it is completely useless to us.  When creating a website (or application) it is important to start from the data and move down (or backwards).  When namespacing it is important to build the namespace from the ground up.  This is where VB and C# differ.  VB.Net assumes the name of the project, C# does not.  Due to the fact that we are referencing the class from inside the project this is neither here nor there.  However, if you were to reference this namespace from outside the project, you would need to start from the lowest level (the project) and move up.
That being said, I own a development company named Recon Mobile Systems.  So this is the logical place to start.  My namespace will be ReconMobileSystem.PDF.DAL.  This is so that I know the code that is in this particular node of the namespace deals with PDFs and it is the Data Access Layer.  Your namespace should encompass the entire class like so:

Imports Microsoft.VisualBasic

Namespace ReconMobileSystems.PDF.DAL
Public Class PDFExport

End Class
End Namespace

Anyone who has read my blog before knows I am a stickler for Regions.  If you are a developer and are not using Regions, there are people looking at your code like a second class citizen.  It is the single easiest method to add a level of professionalism to your code… Think of it this way, if you do not respect your own code enough to spend the time to create Regions, why should I (as your team leader/Director/CTO… boss) take your code seriously?
At any rate, after we add the default Regions for the DAL:

Imports Microsoft.VisualBasic

Namespace ReconMobileSystems.PDF.DAL
Public Class PDFExport
#Region "Globals"

#End Region
#Region
"Properties"

#End Region
#Region
"Methods"

#End Region
#Region
"Helpers"

#End Region
End Class
End Namespace
Now we can go ahead and create the method that returns the data.  For simplicity I am simply going to create a DataTable which is a single table from a DataSet (Thus, DataSet.Tables( 0) is the same as DataTable).  Since we are going to have to return an object, I will create a function.  However, we need to import the correct namespaces (assemblies) so that the Framework knows how to handle these objects.  We also could alternatively use strongly typed objects but here I will simply import:
Imports Microsoft.VisualBasic
Imports System.Data
Since we are only creating a DataTable, we only need to import a single assembly.  Now that we have the reference, we can go ahead and continue with creating our function:
 Public Function getDataTable() As DataTable
getDataTable = New DataTable
getDataTable.Columns.Add("Record Number", GetType(Integer))
getDataTable.Columns.Add(
"Album Name", GetType(String))
getDataTable.Columns.Add(
"Year Produced", GetType(Integer))
getDataTable.Columns.Add(
"Label", GetType(String))
getDataTable.Columns.Add(
"Date Added", GetType(DateTime))

getDataTable.Rows.Add(
1, "Van Halen", 1978, "Warner Bros.", _
DateTime.Now)
getDataTable.Rows.Add(
2, "Van Halen II", 1979, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
3, "Women and Children First", 1980, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
4, "Fair Warning", 1981, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
5, "Diver Down", 1982, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
6, "1984", 1984, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
7, "5150", 1986, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
8, "OU812", 1988, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
9, "For Unlawful Carnal Knowledge", 1991, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
10, "Balance", 1995, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
11, "Van Halen III", 1998, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
12, "A Different Kind of Truth", 2012, _
"Warner Bros.", DateTime.Now)
End Function
Obviously this should go in the methods Region and completes the DAL:
Imports Microsoft.VisualBasic
Imports System.Data


Namespace ReconMobileSystems.PDF.DAL
Public Class PDFExport
#Region "Globals"

#End Region
#Region
"Properties"

#End Region
#Region
"Methods"
Public Function getDataTable() As DataTable
getDataTable = New DataTable
getDataTable.Columns.Add("Record Number", GetType(Integer))
getDataTable.Columns.Add(
"Album Name", GetType(String))
getDataTable.Columns.Add(
"Year Produced", GetType(Integer))
getDataTable.Columns.Add(
"Label", GetType(String))
getDataTable.Columns.Add(
"Date Added", GetType(DateTime))

getDataTable.Rows.Add(
1, "Van Halen", 1978, "Warner Bros.", _
DateTime.Now)
getDataTable.Rows.Add(
2, "Van Halen II", 1979, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
3, "Women and Children First", 1980, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
4, "Fair Warning", 1981, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
5, "Diver Down", 1982, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
6, "1984", 1984, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
7, "5150", 1986, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
8, "OU812", 1988, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
9, "For Unlawful Carnal Knowledge", 1991, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
10, "Balance", 1995, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
11, "Van Halen III", 1998, _
"Warner Bros.", DateTime.Now)
getDataTable.Rows.Add(
12, "A Different Kind of Truth", 2012, _
"Warner Bros.", DateTime.Now)
End Function
#End Region
#Region
"Helpers"

#End Region
End Class
End Namespace

Now that we are done with the DAL, we need to move to the BLL. Double
Left-Click the class (PDFExport) in the BLL. We also need to make a reference to the iTextSharp assembly. You may need to download this from their website which can be found here and is completely free (GNU License). Now that we have the assembly downloaded and extracted, we need to reference the assembly in our project. To do this, you need to Right-Click your project name in the solution explorer | Add | Reference.
image


To the left of the “OK” button there is a Browse… button. Left-Click and navigate to where you saved the iTextSharp.dll file. This is the only assembly we need to reference. Left-Click OK.  Now back to the PDFExport class that is in the BLL. We need to import
The iTextSharp classes that we will use to build our PDF:

Imports Microsoft.VisualBasic
Imports iTextSharp.text
Imports iTextSharp.text.pdf
Imports iTextSharp.text.html.simpleparser

Next we need to add our namespace to the class:

Namespace ReconMobileSystems.PDF.BLL

So in total our class should look like:

Imports Microsoft.VisualBasic
Imports iTextSharp.text
Imports iTextSharp.text.pdf
Imports iTextSharp.text.html.simpleparser
Namespace ReconMobileSystems.PDF.BLL
Public Class PDFExport
End Class
End Namespace

Again, let’s add some professionalism and add our Regions. In the case of the BLL, I (typically) add Globals | Methods | Helpers |
Exports. Then we can add our global variable to the DAL class that we need in order to get the Van Halen (YA!) data:

Imports Microsoft.VisualBasic
Imports iTextSharp.text
Imports iTextSharp.text.pdf
Imports iTextSharp.text.html.simpleparser

Namespace ReconMobileSystems.PDF.BLL
Public Class PDFExport
#Region "Globals"
Dim rmc_DAL As ReconMobileSystems.PDF.DAL.PDFExport = New DAL.PDFExport
#End Region
#Region
"Methods"

#End Region
#Region
"Helpers"

#End Region
#Region
"Exports"

#End Region
End Class
End Namespace

Now that we have our global, in the Methods Region we can create a Function that will call the DAL, run the getDataTable function and then return that object to the original caller. This is accomplished with a function declaration and a single line of code:

Public Function getDataTable() As System.Data.DataTable
getDataTable = rmc_DAL.getDataTable()
End Function

You will notice here that I used a strongly typed function type. This is because I do not expect to re-use the type and therefore do not need to make a complete reference. Here we are setting the function variable to the value returned by the referenced object rmc_DAL which is our PDFExport class in the DAL and finally we are calling the getDataTable method (function) in the DAL. We do not need to create any helpers, so we can move to the presentation layer to create our trigger. Double Left-Click the Default.aspx file located in the project folder. Here we will add a button that will serve as our trigger:

<asp:Button ID="btnTrigger" runat="server" Text="Create PDF" />
So for our Default.aspx file it should look like:
<html xmlns="http://www.w3.org/1999/xhtml">
<
head runat="server">
<
title></title>
</
head>
<
body>
<
form id="form1" runat="server">
<
div>
<
asp:Button ID="btnTrigger" runat="server" Text="Create PDF" />
</
div>
</
form>
</
body>
</
html>

Now that we have created the trigger, we need to code the trigger.
Double Left-Click the Default.aspx.vb file by clicking the triangular
tick in front of the Default.aspx. This will open the back-end code
for our page. From here we need to select our button object from the
dropdown selection for the page:

image

Then select the Click event in the drop down to the right and it will place the event
in your page:
image


Again, we need to practice some professionalism and add some Regions.  On pages I typically use Globals | Page Methods | Page Helpers then we need to move our trigger inside the Page Methods Region. Additionally we can create our global variable that will reference our class in the BLL to get the DataTable and in total our .vb code for our page should look like:

Partial Class _Default
Inherits System.Web.UI.Page
#Region "Globals"
Dim rmc_BLL As ReconMobileSystems.PDF.BLL.PDFExport = New _
ReconMobileSystems.PDF.BLL.PDFExport
#End Region
#Region
"Page Methods"
Protected Sub btnTrigger_Click(sender As Object, e As EventArgs) _
Handles btnTrigger.Click

End Sub
#End Region
#Region
"Page Helpers"

#End Region
End Class
Now comes the meat of the PDF creation phase.  Prior to this was all setup for the one method that will deliver the goods.  We need to move back to the BLL class PDFExport.  Once there we need to create some more imports.  We need to bring in the System.IO, System.Text and since we are here we can pull in the System.Data assemblies.  Now we should look like:
Imports Microsoft.VisualBasic
Imports iTextSharp.text
Imports iTextSharp.text.pdf
Imports iTextSharp.text.html.simpleparser
Imports System.IO
Imports System.Text
Imports System.Data
Generally while creating exports I create some globals that can accessed across the export.  These are objects that I may use several times throughout the export.  In our case, we only need a single method to create and export the PDF.  This being the case, I am going to continue my tradition of creating globals if nothing else than for my conformity.  Obviously, we have moved to the Exports Region and there I will create three objects or global variables.  One to hold the datatable that is created by the DAL, passed to the BLL who then passes it to the presentation layer where it then passes it to our export (whew!).  Secondly, I like to timestamp my exports and usually have the timestamp in the filename so I know when the export was produced.  Here I will create a string variable to hold that value and finally, I will create a string variable to hold the filename of the pdf that we create:
#Region "Exports"
Dim _data As DataTable = New DataTable
Dim runtime As String = String.Empty
Dim filename As String = String.Empty
Now we need to go ahead and create our method which will be a sub or subroutine:
Public Sub PDFExport(ByVal Data As DataTable, _
ByVal RunTime As DateTime, _
ByVal context As HttpContext)
When we use the keyword “Me” in VB or “this” in C# it is basically a reference in memory to the currently active object (or class in this case).  So, if we say Me in a method of our BLL.PDFExport class, we are referring to the PDFExport class.  This being said, we are requiring that in order to use our PDFExport subroutine that they must provide three parameters Data | RunTime | and context.  The first thing we need to do is setup our globals with these values so that they are reusable:
Me._data = Data
Me.runtime = RunTime.ToShortDateString
Now this is my own preference and has nothing to do with exporting the pdf but like previously said, I like to have the date stamp in the file name.  So in this case what I do is convert a DateTime object.  I break down the year | month | day so that I can pad a 0 (zero) in front of the value.  So if you convert a date like January 9, 2015 it will basically convert it to 1/9/2015 as opposed to the much more prettier 01/09/2015, this code will do that:
Dim convertDate As DateTime = RunTime
Dim year As String = convertDate.Year.ToString()
Dim month As String = String.Empty
If convertDate.Month < 10 Then
month = "0" & convertDate.Month.ToString()
Else
month = convertDate.Month.ToString()
End If
Dim
day As String = String.Empty
If convertDate.Day < 10 Then
day = "0" & convertDate.Day.ToString()
Else
day = convertDate.Day.ToString()
End If
Now that we have the correct formatting for our values, we need to go ahead and set our filename:
Me.filename = "c:\temp\PDFTest_" & day & "-" & month & _
"-" & year & ".pdf"
Now.  When any pdf is created, it is created on a BLANK page.  There is no formatting or any setup to the page.  So basically, the next thing we need to do is setup a header.  We do that using what is called a paragraph (if you can guess what that is):
Dim header As Paragraph = New Paragraph("Van Halen Albums", _
New Font(Font.FontFamily.HELVETICA, 22))
header.Alignment =
Element.ALIGN_CENTER
header.SpacingAfter =
30
So basically, we are creating a paragraph with an horizontal alignment of center and will use the Helvetica font with a size of 22 and display the text Van Halen Albums.  Next we need to create a table (aptly name PdfTable) to hold the data from our DataTable:
Dim pdfTable As PdfPTable = New PdfPTable(_data.Columns.Count)
pdfTable.DefaultCell.Padding =
3
pdfTable.WidthPercentage = 95
pdfTable.HorizontalAlignment = Element.ALIGN_CENTER
pdfTable.DefaultCell.BorderWidth =
1
So we are creating the PdfPTable object and then you have to tell it how many columns to create.  You can’t make a breakfast table without knowing the size/dimensions you need.  It works the same way in the virtual world.  Due to the fact that we created the DataTable in the DAL we actually knew how many columns we needed and could have put an integer there, but just in case in the future you find yourself in a dynamic situation, this is the correct way to do it.  The rest of the code is simply parameters that define the table.  The major one to pay attention to is the WidthPercentage.  This controls how much space the table takes up on the page and in the case 95% (width) of the page is populated with the table.  Next we need to take the table and populate the “header” cells of the table with the column names of our datatable.  There are actually no header or footer on the PdfPTable.  We create that using smoke and mirrors with colors and etc..
Dim cell As PdfPCell
For Each column As DataColumn In _data.Columns
cell =
New PdfPCell(New Phrase(column.ColumnName))
cell.BackgroundColor =
New iTextSharp.text.BaseColor(240, 240, 240)
pdfTable.AddCell(cell)
Next
Now we need to go ahead and create and populate the rows of our table from our DataTable:
Dim cell As PdfPCell
For Each column As DataColumn In _data.Columns
cell =
New PdfPCell(New Phrase(column.ColumnName))
cell.BackgroundColor =
New iTextSharp.text.BaseColor(240, 240, 240)
pdfTable.AddCell(cell)
Next
So we are now ready to create the actual PDF Document that will house our header and our table:
Dim pdfDoc As New Document(PageSize.A4, 10.0F, 10.0F, 10.0F, 0.0F)
Since we now have a PdfDoc, we need some place to store it (as opposed to creating a physical file on the system).  We do this with a memoryStream:
Dim memoryStream As MemoryStream = New MemoryStream
You write the PdfDoc to the memoryStream using a pdfWriter:
Dim writer As PdfWriter = PdfWriter.GetInstance(pdfDoc, memoryStream)
So here we are saying to write pdfDoc to the memoryStream as represented in the GetInstance method parameters.  Except we haven’t actually written anything yet.  So now we need to add our header and table objects:
pdfDoc.Open()
pdfDoc.Add(header)
pdfDoc.Add(pdfTable)
pdfDoc.Close()
So now our Pdf Document is created and residing in memory on the server.  How do we get it to the client or user browser?  We use the HttpContext parameter that we required in the PdfExport method:
context.Response.ClearContent()
context.Response.ClearHeaders()
context.Response.ContentType =
"application/pdf"
context.Response.OutputStream.Write(memoryStream.ToArray(), _
0, memoryStream.ToArray().Length)
context.Response.Flush()
context.Response.Close()
The OutputStream.Write method takes three parameters.  The object to be written (our memoryStream that contains the PDF Document), the starting point (why you would start somewhere other than 0, I’m not sure of and have never seen.  0 represents the beginning of the memoryStream), and finally the end point which is the memoryStream to length (or memoryStream.ToArray().Length)


Finally, we need to close the memoryStream so that we do not create any memory leaks:

memoryStream.Close()
Of course, in today’s babysitting world Microsoft has some cleanup routines that run throughout the stack that will prevent us from creating memory leaks but you should not rely on them.  Also it is best practices.  So that is our entire Subroutine to export the file.  In total our BLL.PDFExport class should look like:
Imports Microsoft.VisualBasic
Imports iTextSharp.text
Imports iTextSharp.text.pdf
Imports iTextSharp.text.html.simpleparser
Imports System.IO
Imports System.Text
Imports System.Data

Namespace ReconMobileSystems.PDF.BLL
Public Class PDFExport
#Region "Globals"
Dim rmc_DAL As ReconMobileSystems.PDF.DAL.PDFExport = New _
DAL.PDFExport
#End Region
#Region
"Methods"
Public Function getDataTable() As DataTable
getDataTable = rmc_DAL.getDataTable()
End Function
#End Region
#Region
"Helpers"

#End Region
#Region
"Exports"
Dim _data As DataTable = New DataTable
Dim runtime As String = String.Empty
Dim filename As String = String.Empty

Public Sub PDFExport(ByVal Data As DataTable, _
ByVal RunTime As DateTime, _
ByVal context As HttpContext)
Me._data = Data
Me.runtime = RunTime.ToShortDateString

Dim convertDate As DateTime = RunTime
Dim year As String = convertDate.Year.ToString()
Dim month As String = String.Empty
If convertDate.Month < 10 Then
month = "0" & convertDate.Month.ToString()
Else
month = convertDate.Month.ToString()
End If
Dim
day As String = String.Empty
If convertDate.Day < 10 Then
day = "0" & convertDate.Day.ToString()
Else
day = convertDate.Day.ToString()
End If

Me
.filename = "c:\temp\PDFTest_" & day & "-" & _
month &
"-" & year & ".pdf"

Dim header As Paragraph = New _
Paragraph("Van Halen Albums", New Font(Font.FontFamily.HELVETICA, 22))
header.Alignment =
Element.ALIGN_CENTER
header.SpacingAfter =
30

Dim pdfTable As PdfPTable = New _
PdfPTable(_data.Columns.Count)
pdfTable.DefaultCell.Padding =
3
pdfTable.WidthPercentage = 95
pdfTable.HorizontalAlignment = Element.ALIGN_CENTER
pdfTable.DefaultCell.BorderWidth =
1

Dim cell As PdfPCell
For Each column As DataColumn In _data.Columns
cell =
New PdfPCell(New Phrase(column.ColumnName))
cell.BackgroundColor =
New _
iTextSharp.text.BaseColor(240, 240, 240)
pdfTable.AddCell(cell)
Next
For Each
row As DataRow In _data.Rows
pdfTable.AddCell(
CInt(row(0)))
pdfTable.AddCell(row(
1).ToString())
pdfTable.AddCell(
CInt(row(2)))
pdfTable.AddCell(row(
3).ToString())
pdfTable.AddCell(
CDate(row(4)).ToShortDateString)
Next


Dim
pdfDoc As New _
Document(PageSize.A4, 10.0F, 10.0F, 10.0F, 0.0F)
Dim memoryStream As MemoryStream = New MemoryStream
Dim writer As PdfWriter = PdfWriter.GetInstance(pdfDoc, _
memoryStream)
pdfDoc.Open()
pdfDoc.Add(header)
pdfDoc.Add(pdfTable)
pdfDoc.Close()

context.Response.ClearContent()
context.Response.ClearHeaders()
context.Response.ContentType =
"application/pdf"
context.Response.OutputStream.Write(memoryStream.ToArray(), _
0, memoryStream.ToArray().Length)
context.Response.Flush()
context.Response.Close()

memoryStream.Close()

End Sub
#End Region
End Class
End Namespace
Unfortunately, we are not creating PDFs quite yet.  We still have to setup the trigger.  Jump back to the Default.aspx.vb file.  We need to add a line of code to the Click trigger that will execute our BLL | DAL | create the datatable and pass it back to the BLL | pass the table to the Presentation Layer | Executes the PDFExport Subroutine in our BLL.  Quite honestly, this is rather easy since we already have all the other supporting code in place:
rmc_BLL.PDFExport(rmc_BLL.getDataTable(), DateTime.Now, _
HttpContext.Current)
So our entire back-end page code should look like:
Partial Class _Default
Inherits System.Web.UI.Page
#Region "Globals"
Dim rmc_BLL As ReconMobileSystems.PDF.BLL.PDFExport = New _
ReconMobileSystems.PDF.BLL.PDFExport
#End Region
#Region
"Page Methods"
Protected Sub btnTrigger_Click(sender As Object, e As EventArgs) _
Handles btnTrigger.Click
rmc_BLL.PDFExport(rmc_BLL.getDataTable(),
DateTime.Now, _
HttpContext.Current)
End Sub
#End Region
#Region
"Page Helpers"

#End Region
End Class
Press F5 and run the application.  You should see a button to click and then you should be well versed in the albums that Van Halen has produced:


image


 


 


 


Håþþ¥ .ñꆆïñg…

No comments:

Post a Comment