Creating a Purchase Order PDF

Introduction

In the previous post we established that Syncfusion PDF would allow us to create a generic pdf document and save it to the server.  In this post we are going to return to the Purchase Order application and create and save a pdf of the purchase order.

Once we have created, and saved, a PDF of the purchase order we will extend the Email function to attach the PDF purchase order to the email.

The PDF of the purchase order should look like this:

Preparation

  • Open the Purchase Order project in Visual Studio 19
  • Using NuGet Package Manager add "Syncfusion.Pdf.Net.Core" the project.
  • Add a new folder called 'Files'; this is where the PDFs will be saved.
  • Add a new folder called 'Media' and copy into it the image to be used as the logo.

NOTE - The Files and Media folders must be placed under wwwroot if the application is to be published to Azure.

Add a 'PDF' button to the Index Page

To make reviewing progress during the development stage as easy as possible we will add a button called 'PDF' to the Index page.  When an order is selected clicking the 'PDF' button will produce a PDF document and save it to the 'Files' folder.

Open Index.razor.  In the 'OnInitializedAsync' method add the following line to add the 'PDF' button.

Toolbaritems.Add(new ItemModel() { Text = "PDF", TooltipText = "PDF Test", PrefixIcon = "e-print" });

In the ToolbarClickHandler method add the following 

if (args.Item.Text == "PDF")
{
    //Code for editing - Check that an Order has been selected from the grid
    if (selectedPOHeaderID == 0)
    {
        WarningHeaderMessage = "Warning!";
        WarningContentMessage = "Please select an Order from the grid.";
        Warning.OpenDialog();
        return;
    }

    //Populate orderHeader using selectedPOHeaderID
    orderHeader = await POHeaderService.POHeader_GetOne(selectedPOHeaderID);

    //Populate orderLines using selectedPOHeaderID
    orderLinesByPOHeader = await POLineService.POLine_GetByPOHeader(selectedPOHeaderID);
    orderLines = orderLinesByPOHeader.ToList(); //Convert from IEnumerable to List

    //Pass orderHeader and orderLines to exportservice.CreatePdf and get back a MemoryStream of the pdf document
    using (MemoryStream pdfStream = exportService.CreatePdf(orderHeader, orderLines))
    {
        //Get current directory and concatenate with 'Files' folder and selected Order Number to create pdf file name
        string filename = @$"{Environment.CurrentDirectory}/wwwroot/Files/PO-" + selectedPOHeaderOrderNumber + ".pdf";

        //Define a FileStream object
        FileStream fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None);

        //Convert MemoryStream to a byte array
        byte[] pdfData = pdfStream.ToArray();

        //Write the byte array to the FileStream (Disc) and close the Filestream
        fs.Write(pdfData, 0, pdfData.Length);
        fs.Close();
    }
}

The code should look similar to this:

At the top of Index.razor, we need to add the following lines.  (We will be adding the CreatePurchaseOrderPdf class in the next section.)

@using System.IO;

@inject CreatePurchaseOrderPdf exportService
@inject IPOLineService POLineService

The top of the file should look similar to this:

We will be passing OrderLines to the CreatePurchaseOrderPdf class, so we must declare a list object of orderLines to enable this to be processed.  Add the following somewhere near the top of the code section.

public List<POLine> orderLines = new List<POLine>();

CreatePurchaseOrderPdf

Add a new class in the Data folder called 'CreatePurchaseOrderPdf' and copy the following code into the class.  This looks intimidating, but is mainly verbose and repetitious.

using System;
using System.Collections.Generic;
using Syncfusion.Pdf;
using Syncfusion.Pdf.Graphics;
using Syncfusion.Pdf.Grid;
using Syncfusion.Drawing;
using System.IO;

namespace BlazorPurchaseOrders.Data
{    public class CreatePurchaseOrderPdf
    {
        public MemoryStream CreatePdf(POHeader orderHeader, List<POLine> orderLines)
        {
            //Create a new PDF document
            using (PdfDocument pdfDocument = new())
            {
                int paragraphAfterSpacing = 8;
                int cellMargin = 4;

                //Add page to the PDF document
                PdfPage page = pdfDocument.Pages.Add();

                //Set the page size
                pdfDocument.PageSettings.Size = PdfPageSize.A4;
                pdfDocument.PageSettings.Margins.Left = 28;
                pdfDocument.PageSettings.Margins.Right = 28;
                pdfDocument.PageSettings.Margins.Top = 8;
                pdfDocument.PageSettings.Margins.Bottom = 14;

                #region Header

                //Define 'bounds' for header
                RectangleF boundsHeader = new (0, 0, pdfDocument.Pages[0].GetClientSize().Width, 200);
                PdfPageTemplateElement header = new(boundsHeader);
                pdfDocument.Template.Top = header;

                //Load the image from the disc                
                string logoFilename = @$"{Environment.CurrentDirectory}/wwwroot/Media/blazorcode_logo_small.png";    
                FileStream logoStream = new(logoFilename, FileMode.Open, FileAccess.Read);
                PdfBitmap logo = new(logoStream);

                //Draw the image -- SizeF scales the image to specified size
                header.Graphics.DrawImage(logo, new PointF(0, 0), new SizeF(183, 35));               

                //Create a new font
                PdfStandardFont companyAddressFont = new(PdfFontFamily.Helvetica, 10, PdfFontStyle.Bold);

                //Set the format for string
                PdfStringFormat stringFormat = new();

                //Set the text alignment
                stringFormat.Alignment = PdfTextAlignment.Right;

                string adddressText = "blazorcode.uk";
                header.Graphics.DrawString(adddressText, companyAddressFont, PdfBrushes.Black, new RectangleF(0, 0, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);

                adddressText = "5 High Street";
                header.Graphics.DrawString(adddressText, companyAddressFont, PdfBrushes.Black, new RectangleF(0, 14, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);

                adddressText = "Trumpington";
                header.Graphics.DrawString(adddressText, companyAddressFont, PdfBrushes.Black, new RectangleF(0, 28, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);

                adddressText = "TR4A IRS";
                header.Graphics.DrawString(adddressText, companyAddressFont, PdfBrushes.Black, new RectangleF(0, 42, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);

                //Concatenate Supplier Name & Address, omitting blank address lines
                string supplierAddress = orderHeader.SupplierName + Environment.NewLine;

                if (!String.IsNullOrEmpty(orderHeader.POHeaderSupplierAddress1))
                {
                    supplierAddress += orderHeader.POHeaderSupplierAddress1 + Environment.NewLine;
                }
                if (!String.IsNullOrEmpty(orderHeader.POHeaderSupplierAddress2))
                {
                    supplierAddress += orderHeader.POHeaderSupplierAddress2 + Environment.NewLine;
                }
                if (!String.IsNullOrEmpty(orderHeader.POHeaderSupplierAddress3))
                {
                    supplierAddress += orderHeader.POHeaderSupplierAddress3 + Environment.NewLine;
                }
                supplierAddress += orderHeader.POHeaderSupplierPostCode;

                //Set the text alignment and style
                stringFormat.Alignment = PdfTextAlignment.Left;                
                PdfStandardFont headerFont = new(PdfFontFamily.Helvetica, 9, PdfFontStyle.Regular);

                //Print Supplier Name & Address
                header.Graphics.DrawString(supplierAddress, headerFont, PdfBrushes.Black, new RectangleF(0, 90, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);

                //Order No etc
                header.Graphics.DrawString("Order No", headerFont, PdfBrushes.Black, new RectangleF((page.GetClientSize().Width) / 2, 90, page.GetClientSize().Width / 2, 18), stringFormat);
                header.Graphics.DrawString("Order Date", headerFont, PdfBrushes.Black, new RectangleF((page.GetClientSize().Width) / 2, 108, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);
                header.Graphics.DrawString("Contact", headerFont, PdfBrushes.Black, new RectangleF((page.GetClientSize().Width) / 2, 126, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);

                header.Graphics.DrawString(": " + orderHeader.POHeaderOrderNumber.ToString(), headerFont, PdfBrushes.Black, new RectangleF(((page.GetClientSize().Width) / 2) + 50, 90, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);
                header.Graphics.DrawString(": " + orderHeader.POHeaderOrderDate.ToShortDateString(), headerFont, PdfBrushes.Black, new RectangleF(((page.GetClientSize().Width) / 2) + 50, 108, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);
                header.Graphics.DrawString(": " + orderHeader.POHeaderRequestedBy, headerFont, PdfBrushes.Black, new RectangleF(((page.GetClientSize().Width) / 2) + 50, 126, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);

                //Purchase Order Title
                stringFormat.Alignment = PdfTextAlignment.Center;
                PdfStandardFont docTypeFont = new(PdfFontFamily.Helvetica, 14, PdfFontStyle.Bold);
                header.Graphics.DrawString("PURCHASE ORDER", docTypeFont, PdfBrushes.Black, new RectangleF(0, 170, page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);

                #endregion

                #region Body

                PdfStandardFont contentFont = new(PdfFontFamily.Helvetica, 8);

                decimal runningNetPrice = 0;
                decimal runningTaxAmount = 0;
                decimal runningGrossPrice = 0;
                float bottomOfGrid = 0;
                int linesPerPage = 40;

                //Calculate the number of pages                
                int pageCount = ((orderLines.Count + (linesPerPage - 1)) / linesPerPage);

                //Initialise pageNo and the line number
                int pageNo = 1;
                int i = 0;          //Effectively line number

                // Deal with full pages first - (if only one page is needed, see next section)
                while (pageNo < pageCount)
                {
                    //Create a PdfGrid
                    PdfGrid pdfGrid = new();
                    pdfGrid.Style.CellPadding.Left = cellMargin;
                    pdfGrid.Style.CellPadding.Right = cellMargin;

                    //Sets up columns, column alignment and header text
                    CreatePdfGridLayout(pdfGrid);

                    while (i < (pageNo * linesPerPage))                        
                    {
                        string truncatedProductCode = orderLines[i].POLineProductCode;
                        string truncatedProductDescription = orderLines[i].POLineProductDescription;
                        SizeF sizeProductCode = contentFont.MeasureString(orderLines[i].POLineProductCode);
                        SizeF sizeProductDescription = contentFont.MeasureString(orderLines[i].POLineProductDescription);

                        //Product Code Column Width (minus cellMargin) = 70 - (2*4) = 62
                        if (sizeProductCode.Width > 62)
                        {
                            truncatedProductCode = GetFittedStringToPrint(truncatedProductCode, 62, contentFont);
                        }

                        //Product Description Column Width (minus cellMargin) = 130 - (2*4) = 122
                        if (sizeProductDescription.Width > 122)
                        {
                            truncatedProductDescription = GetFittedStringToPrint(truncatedProductDescription, 122, contentFont);
                        }

                        //Add rows
                        PdfGridRow pdfGridRow = pdfGrid.Rows.Add();
                        pdfGridRow.Cells[0].Value = truncatedProductCode;
                        pdfGridRow.Cells[1].Value = truncatedProductDescription;
                        pdfGridRow.Cells[2].Value = String.Format("{0:N0}", orderLines[i].POLineProductQuantity);
                        pdfGridRow.Cells[3].Value = String.Format("{0:C2}", orderLines[i].POLineProductUnitPrice);
                        pdfGridRow.Cells[4].Value = String.Format("{0:C2}", orderLines[i].POLineNetPrice);
                        pdfGridRow.Cells[5].Value = String.Format("{0:P2}", orderLines[i].POLineTaxRate);
                        pdfGridRow.Cells[6].Value = String.Format("{0:C2}", orderLines[i].POLineTaxAmount);
                        pdfGridRow.Cells[7].Value = String.Format("{0:C2}", orderLines[i].POLineGrossPrice);

                        runningNetPrice = (decimal)(runningNetPrice + orderLines[i].POLineNetPrice);
                        runningTaxAmount += orderLines[i].POLineTaxAmount;
                        runningGrossPrice += orderLines[i].POLineGrossPrice;

                        i++;
                    }

                    //Draw PDF grid into the PDF page
                    PdfLayoutResult result = pdfGrid.Draw(page, new PointF(0, paragraphAfterSpacing));

                    if (pageNo < pageCount)
                    {                   
                        page = pdfDocument.Pages.Add();
                    }

                    //Increment page no
                    pageNo++;
                }

                //Now deal with last records, those that will not fill a whole page                
                PdfGrid pdfGridLastPage = new();
                pdfGridLastPage.Style.CellPadding.Left = cellMargin;
                pdfGridLastPage.Style.CellPadding.Right = cellMargin;

                //Sets up columns, column alignment and header text
                CreatePdfGridLayout(pdfGridLastPage);

                PdfGraphics graphicsLastPage = page.Graphics;

                while (i < orderLines.Count)
                {                    
                    string truncatedProductCode = orderLines[i].POLineProductCode;
                    string truncatedProductDescription = orderLines[i].POLineProductDescription;
                    SizeF sizeProductCode = contentFont.MeasureString(orderLines[i].POLineProductCode);
                    SizeF sizeProductDescription = contentFont.MeasureString(orderLines[i].POLineProductDescription);

                    //Product Code Column Width (minus cellMargin) = 70 - (2*4) = 62
                    if (sizeProductCode.Width > 62)
                    {
                        truncatedProductCode = GetFittedStringToPrint(truncatedProductCode, 62, contentFont);
                    }

                    //Product Description Column Width (minus cellMargin) = 130 - (2*4) = 122
                    if (sizeProductDescription.Width > 122)
                    {
                        truncatedProductDescription = GetFittedStringToPrint(truncatedProductDescription, 122, contentFont);
                    }

                    //Add rows
                    PdfGridRow pdfGridRow = pdfGridLastPage.Rows.Add();
                    pdfGridRow.Cells[0].Value = truncatedProductCode;
                    pdfGridRow.Cells[1].Value = truncatedProductDescription;
                    pdfGridRow.Cells[2].Value = String.Format("{0:N0}", orderLines[i].POLineProductQuantity);
                    pdfGridRow.Cells[3].Value = String.Format("{0:C2}", orderLines[i].POLineProductUnitPrice);
                    pdfGridRow.Cells[4].Value = String.Format("{0:C2}", orderLines[i].POLineNetPrice);
                    pdfGridRow.Cells[5].Value = String.Format("{0:P2}", orderLines[i].POLineTaxRate);
                    pdfGridRow.Cells[6].Value = String.Format("{0:C2}", orderLines[i].POLineTaxAmount);
                    pdfGridRow.Cells[7].Value = String.Format("{0:C2}", orderLines[i].POLineGrossPrice);

                    runningNetPrice = (decimal)(runningNetPrice + orderLines[i].POLineNetPrice);
                    runningTaxAmount += orderLines[i].POLineTaxAmount;
                    runningGrossPrice += orderLines[i].POLineGrossPrice;

                    //Increment order line no
                    i++;
                }

                //Draw PDF grid into the PDF page
                PdfLayoutResult resultLastPage = pdfGridLastPage.Draw(page, new PointF(0, paragraphAfterSpacing));

                bottomOfGrid = resultLastPage.Bounds.Bottom;

                //Section for showing totals
                string totalNetPrice = String.Format("{0:C2}", runningNetPrice);
                string totalTaxAmount = String.Format("{0:C2}", runningTaxAmount);
                string totalGrossPrice = String.Format("{0:C2}", runningGrossPrice);

                stringFormat.Alignment = PdfTextAlignment.Right;
                PdfStandardFont totalsFont = new(PdfFontFamily.Helvetica, 8, PdfFontStyle.Bold);

                graphicsLastPage.DrawString(totalNetPrice, totalsFont, PdfBrushes.Black, new RectangleF(295 - cellMargin, bottomOfGrid + 10, 60, page.GetClientSize().Height), stringFormat);
                graphicsLastPage.DrawString(totalTaxAmount, totalsFont, PdfBrushes.Black, new RectangleF(395 - cellMargin, bottomOfGrid + 10, 60, page.GetClientSize().Height), stringFormat);
                graphicsLastPage.DrawString(totalGrossPrice, totalsFont, PdfBrushes.Black, new RectangleF(455 - cellMargin, bottomOfGrid + 10, 60, page.GetClientSize().Height), stringFormat);

                stringFormat.Alignment = PdfTextAlignment.Left;
                graphicsLastPage.DrawString("Order Total", totalsFont, PdfBrushes.Black, new RectangleF(0 + cellMargin, bottomOfGrid + 10, 60, page.GetClientSize().Height), stringFormat);

                //Create new PDF pen
                PdfPen pen = new(Color.Black, 1);

                //Draw line on the page below the Order Totals line
                graphicsLastPage.DrawLine(pen, 0, bottomOfGrid + 20, 515, bottomOfGrid + 20);

                #endregion

                #region Footer

                //Define 'bounds' for footer
                RectangleF boundsFooter = new(0, pdfDocument.Pages[0].GetClientSize().Height, 
                    pdfDocument.Pages[0].GetClientSize().Width, 10);
                PdfPageTemplateElement footer = new (boundsFooter);
                pdfDocument.Template.Bottom = footer;

                PdfStandardFont footerFont = new (PdfFontFamily.Helvetica, 7);
                PdfBrush brush = new PdfSolidBrush(Color.Black);               

                //Page x of y
                //string footerPageNumber = $"Page {pageNo} of {pageCount}";
                //stringFormat.Alignment = PdfTextAlignment.Center;
                //footer.Graphics.DrawString(footerPageNumber, footerFont, brush, new RectangleF(0, 0, 
                //    page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);

                //Create date printed field
                string datePrinted = "Printed on: " + DateTime.Now.ToString("dd/MM/yyyy");
                stringFormat.Alignment = PdfTextAlignment.Right;
                footer.Graphics.DrawString(datePrinted, footerFont, brush, new RectangleF(0, 0, 
                    page.GetClientSize().Width, page.GetClientSize().Height), stringFormat);

                #endregion

                //Alternative version for page numbers using PdfCompositeField
                ///Create page number and count fields
                PdfPageNumberField pageNumber = new();
                PdfPageCountField count = new();
                //Create composite field.
                //Add the fields in composite fields
                PdfCompositeField compositeField = new(footerFont, brush, "Page {0} of {1}", pageNumber, count);

                //Draw the composite fields in footer.  Note they are both on the same 'line'.
                compositeField.Draw(footer.Graphics, new PointF(240, 0));

                using (MemoryStream stream = new ())
                {
                    //Saving the PDF document into the stream
                    pdfDocument.Save(stream);
                    //Closing the PDF document
                    pdfDocument.Close(true);
                    return stream;
                }

                static string GetFittedStringToPrint(string input, float maxWidth, PdfStandardFont contentFont)
                {
                    var output = input;
                    var stringMeasurement = contentFont.MeasureString(input); 
                    if (stringMeasurement.Width > maxWidth)
                    {
                        var inputMinusOneChar = input;
                        do
                        {
                            inputMinusOneChar = inputMinusOneChar.Substring(0, inputMinusOneChar.Length - 1).Trim();
                            stringMeasurement = contentFont.MeasureString(inputMinusOneChar + "...");
                        } while (stringMeasurement.Width > maxWidth);
                        output = inputMinusOneChar + "...";
                    }
                    return output;
                }

                static void CreatePdfGridLayout(PdfGrid pdfGrid)
                {
                    //Create PDF grid column.   NOTE: A4 Page width between standard margins = 515 points
                    PdfGridColumn column1 = new(pdfGrid);
                    column1.Width = 70;
                    PdfGridColumn column2 = new(pdfGrid);
                    column2.Width = 130;
                    PdfGridColumn column3 = new(pdfGrid);
                    column3.Width = 45;
                    PdfGridColumn column4 = new(pdfGrid);
                    column4.Width = 50;
                    PdfGridColumn column5 = new(pdfGrid);
                    column5.Width = 60;
                    PdfGridColumn column6 = new(pdfGrid);
                    column6.Width = 50;
                    PdfGridColumn column7 = new(pdfGrid);
                    column7.Width = 50;
                    PdfGridColumn column8 = new(pdfGrid);
                    column8.Width = 60;

                    //Add eight columns.
                    pdfGrid.Columns.Add(column1);
                    pdfGrid.Columns.Add(column2);
                    pdfGrid.Columns.Add(column3);
                    pdfGrid.Columns.Add(column4);
                    pdfGrid.Columns.Add(column5);
                    pdfGrid.Columns.Add(column6);
                    pdfGrid.Columns.Add(column7);
                    pdfGrid.Columns.Add(column8);

                    //Set alignment of columns 3 to 8
                    column3.Format.Alignment = PdfTextAlignment.Right;
                    column4.Format.Alignment = PdfTextAlignment.Right;
                    column5.Format.Alignment = PdfTextAlignment.Right;
                    column6.Format.Alignment = PdfTextAlignment.Right;
                    column7.Format.Alignment = PdfTextAlignment.Right;
                    column8.Format.Alignment = PdfTextAlignment.Right;

                    //Add header.
                    pdfGrid.Headers.Add(1);
                    PdfGridRow pdfGridHeader = pdfGrid.Headers[0];
                    pdfGridHeader.Cells[0].Value = "Code";
                    pdfGridHeader.Cells[1].Value = "Description";
                    pdfGridHeader.Cells[2].Value = "Quantity";
                    pdfGridHeader.Cells[3].Value = "Unit Price";
                    pdfGridHeader.Cells[4].Value = "Net Price";
                    pdfGridHeader.Cells[5].Value = "Tax Rate";
                    pdfGridHeader.Cells[6].Value = "Tax";
                    pdfGridHeader.Cells[7].Value = "Total";

                    //Applying built-in style to the PDF grid
                    pdfGrid.ApplyBuiltinStyle(PdfGridBuiltinStyle.GridTable4Accent1);

                    //We want the grid header to be in bold.  To do this:
                    //Create an instance of PdfGridRowStyle 
                    PdfGridRowStyle pdfGridRowStyle = new ();
                    pdfGridRowStyle.Font = new PdfStandardFont(PdfFontFamily.Helvetica, 8, PdfFontStyle.Bold);
                    //Set style for the header 
                    pdfGrid.Headers.ApplyStyle(pdfGridRowStyle);
                }
            }
        }
    }
}

Before delving into the above code, add the following line to Startup.cs under the other 'services' lines:

services.AddSingleton<CreatePurchaseOrderPdf>();

The code for the 'CreatePurchaseOrderPdf' class is extensive, but can be broken down into a number of elements or sections.

The method 'CreatePdf' receives orderHeader and orderLines objects with the bulk of the work taking place within this method.  A new PdfDocument is declared (just called PdfDocument) within the 'using' statement.  A couple of variables are declared for paragraph spacing and cell margins.

A PdfDocument comprises a number of PdfPages, so a page is added to the document followed by the page size and margins.

Each page is split into 3 sections, header, body and footer.

The header comprises text and images.  Images, text and other objects, such as a grids, can be placed on the page either by 'Point' or 'Rectangle'.  A 'Point' is defined by an x and y position and a 'Rectangle' by an x and y position plus width and height.  All x, y, width and height units are measured in 'points'.  I have found that an A4 page with left and right margins of 28 points each has about 515 points between margins.  The x and y coordinates for a 'Point' or 'Rectangle' are the top right corner.  I have also found using rectangles to place text to be the most convenient; for example, by placing text within a rectangle it is easy to have items right-aligned.

PdfPageTemplateElement

The 'header' section is defined as a 'PDFPageTemplateElement' within a specified rectangle. 

//Define 'bounds' for header
RectangleF boundsHeader = new (0, 0, pdfDocument.Pages[0].GetClientSize().Width, 200);
PdfPageTemplateElement header = new(boundsHeader);
pdfDocument.Template.Top = header;

Text

Text is placed on the page by using the general pattern of:

Graphics.DrawString(string, font, brush colour, position, format) 

string

The text to be printed, either as a string variable or the actual text in quotes

font

Normally pre-defined in the pattern shown below, in this case for a font to be used for the company address.  It comprises Font name, size and style.

PdfStandardFont companyAddressFont = new(PdfFontFamily.Helvetica, 10, PdfFontStyle.Bold);
brush colour

The colour of the text, although it can be pre-defined, it is normally entered directly, e.g. black is entered as "PdfBrushes.Black".  It can be pre-defined in this format, but there is no great advantage:

PdfBrush brushName = new PdfSolidBrush(Color.Black);
position

I have adopted the practice of placing text within a rectangle, the general format of which looks like:

RectangleF(x, y, width, height)

The advantage of using a rectangle is that the text can then be aligned both horizontally, but also vertically if required.  In practice I haven't specified width or height, instead using the following:

RectangleF(0, 0, page.GetClientSize().Width, page.GetClientSize().Height)

In this "page.GetClientSize().Width" and "page.GetClientSize().Height" get the available width and height of the page section.  It might seem counter-intuitive to specify the whole width and height, but it seems to work and saves calculating the required width and height. 

format

PdfStringFormat allows amongst other parameters, horizontal and vertical text alignment as well as line spacing and other more specialised requirements.  I have used it almost exclusively for right-aligning text, as follows:

//Set the format for string
PdfStringFormat stringFormat = new();

//Set the text alignment
stringFormat.Alignment = PdfTextAlignment.Right;

Images

I want a logo placed in the top left-hand corner of the purchase order.  To prepare for this place the logo image file in a local folder:

  • Add a new folder called 'Media' under the 'wwwroot'.
  • Place the required logo file in the Media folder.  My logo is called 'blazorcode_logo_small.png'

There are several ways of placing an image on a page, but I have chosen the following general pattern of:

Graphics.DrawImage(image, point, size)

image

it seems that 'image' can either be PdfImage or PdfBitmap, but the first example given by Syncfusion is PdfBitmap so I have chosen that.  The technique is to read the file from disc as a FileStream and then to convert to PdfBitmap as shown here:

//Load the image from the disc                
string logoFilename = @$"{Environment.CurrentDirectory}/wwwroot/Media/blazorcode_logo_small.png";    
FileStream logoStream = new(logoFilename, FileMode.Open, FileAccess.Read);
PdfBitmap logo = new(logoStream);
point

Point is expressed as PointF(x, y) where z and y are co-ordinates in points.

size

Size is similarly expressed as SizeF(width, height) where width and height are also expressed in points. By using 'size' it allows the original image to be re-sized if necessary.

The code to place the image in the very top left-hand corner of the header section is therefore :

//Draw the image -- SizeF scales the image to specified size
header.Graphics.DrawImage(logo, new PointF(0, 0), new SizeF(183, 35));    

Adding Logo and Text

Using the techniques described above it is then a matter of going through each element required for the header, either fixed text or data from the orderHeader object and placing each element in the required poisition.

The only item of note is the method used to ensure that blank address lines are suppressed.  A string variable called 'supplierAddress' is declared and assigned the value of 'SupplierName' from orderHeader and a new line appended (there will always be a supplier name).  Each address line from orderHeader is then tested to check it is not null or empty.  If it is null or empty, it is skipped, otherwise its value is appended to 'supplierAddress' together with a new line.

string supplierAddress = orderHeader.SupplierName + Environment.NewLine;

if (!String.IsNullOrEmpty(orderHeader.POHeaderSupplierAddress1))
{
    supplierAddress += orderHeader.POHeaderSupplierAddress1 + Environment.NewLine;
}

Body

The body of the purchase order will consist of a grid (PdfGrid) with a header row showing column names followed by a row for each purchase order line.  Under the grid we will display totals for appropriate columns.  (I couldn't find an easy way to show totals as the last row of the grid, besides, I think this looks better.)

Counting Pages

The main challenge with this layout is that it is possible that the number of purchase order lines will exceed the number of rows that can be fitted to a page.  Where this is the case we will want second and subsequent pages to have the main 'header' section with the logo, supplier name and address etc. (and footer - yet to be covered), as well as displaying the grid column names as the first row of the grid.  We must also ensure that the totals are displayed on the last page only.

The first thing to do in this scenario is to calculate the number of pages required.  This can be achieved using the following formula:

No of pages = ((Total no of order lines + (Lines per page - 1)) / Lines per page)

This relies on the fact that in C# integer division always effectively rounds down.  To take an example, if we have 75 lines and can fit 50 rows per page we get:

No of pages = ((75 + 49) / 50) = 124/50 = 2 (rounding down)

As another example say we can fit 25 rows per page, we get:

No of pages = ((75 + 24) / 25) = 99/25 = 3 (rounding down)

As well as calculating the number of pages we also declare variables to hold the running totals we need and set the number of lines per page (this was determined more by trial and error than anything more sophisticated!)

Full Pages

We will deal with full pages first (they won't need a final row to display totals).  We add a grid and define the layout (i.e. column widths, text alignment within columns and column headings and font style) using the separate method "CreatePdfGridLayout".

Having set up the grid we then use a 'while' loop to cycle through the order lines until we reach the 'page no x no of lines per page', adding a grid row and adding to the running totals before finally incrementing the 'order line number'.

There is however, a 'catch' that needs to be dealt with: what happens if a product code or description is so long it would word-wrap within a column? If this happened the vertical space used by the grid would increase and we would potentially end up with the grid not fitting on the page.  Rather than trying to keep count of the number of wrapped lines I decided to truncate any text that was about to wrap and let the user know it had been truncated by appending an ellipsis (...).  The two columns where this could happen are the Product Code and Product Description.

The code that controls this sets up variables for the length of the product code and description and uses the 'MeasureString' method to get the length of the strings (in points).  These are then compared with the column widths (minus the cell margins) and if greater than that the 'GetFittedStringToPrint' method is called to reduce the text by one character at a time (and adding '...') comparing the modified length with the available space.  This cycle is repeated until the shortened string fits the column width.  The two sections of code that achieve this are:

Last Page

The 'Last Page' could well be the first page; the main difference between the last page and any preceding pages being that the last page has a line of text below the grid showing the purchase order totals.

The code for the 'Last Page' therefore follows closely the code for 'Full pages', however to differentiate the last page from others I have named the PdfGrid 'pdfGridLastPage' and PdfGraphics 'graphicsLastPage' - I don't think this is strictly necessary, but it helped me keep track of where I was!

After cycling through orderLines, and reaching the last one, the grid is placed on the page and at the same time the position of the bottom of the grid recorded.  String variables are declared for the totals and the running totals formatted in currency and assigned to these variables.  The variables are then placed on the page using the 'DrawString' method, together with a string with the value of 'Order Totals'.

Lastly a line is drawn under the totals.

The footer section will be used simply to show "Page x of y" and the date the document was printed.  To do this we:

  • Set the boundaries of the footer by:
    • Defining a rectangle (see below)
    • Declare a 'PdfPageTemplateElement' with the parameter of the rectangle declared in the first step.
    • Set the 'pdfDocument.Template.Bottom' to be Template Element declared above
  • Declare the font and brush for the footer
  • Construct the text to be displayed ($"Page {pageNo} of {pageCount}")
    • Set the alignment of the text to be printed within the rectangle (Centred)
    • Use 'DrawString' to print the text
  • Construct the text to be displayed ($"Printed on: " + DateTime.Now.ToString("dd/MM/yyyy"))
    • Set the alignment of the text to be printed within the rectangle
      Use 'DrawString' to print the text

Note that I have set the height of the rectangle to be used as the boundary of the footer to be 10 points, and the y co-ordinate is: pdfDocument.Pages[0].GetClientSize().Height

The code looks like this:

Return the PDF as MemoryStream

Having created the PDF document, we must now return it to the calling procedure (from Index.razor) as a MemoryStream.

Adding the PDF to the Email

There are a few changes that need to be made to the project to attach a PDF of the purchase order to an email sent to the supplier.

Changes to Index.razor

The first change needed is to modify the ToolBarClickHandler method for the email option.  We start by getting the data required for the 'CreatePdf' method and then calling that method.  This will create the PDF and save it to disc.  (It is basically the same as the code we created for the 'PDF' toolbar button, with the exception that 'filename' is declared outside the 'using ... CreatePdf' because we will need it later to pass the the email method.)

Next, I have changed the 'emailbody' text to include a reference to the attached PDF.

The EmailSendMethod for both MailKit and SendGrid will need amending to attach the PDF; we need to pass to both methods the actual name of the PDF and the name we would like to use when attached to the email.  I have therefore declared a new string called 'attachmentName' and assigned it a value of the phrase 'Purchase Order:' followed by the order number and finally added the '.pdf' suffix.

As mentioned above, changes are now required to both EmailSenderMailKit.cs and EMailSenderSendGrid.cs.

EmailSenderMailKit

The code is shown below (and is also included in the Code page).  The 'filename' and 'attachmentName' are passed to the method, and a 'BodyBuilder' object included to allow the PDF file to be attached.

EmailSenderSendGrid

As with MailKit, the filename and attachmentName are passed to the SendGrid email sender, but the method of attaching the file is marginally simpler, as shown below:

Finally...

I don't want the saved PDF files to be retained on the server once they have been attached to an email and sent.  To remove them we just need the following after calling the email methods. 

Lastly, we used the PDF button on the Index page during development, but it is no longer required.  For simplicity, comment out the 'Toolbaritems' line in the OnInitilaizedAsync method for 'PDF'.  Although I'm not keen on leaving redundant code in a project, I have left the code in 'ToolbarClickHandler' in case I need to reinstate it later.

Code

Complete code for all files changed in this post can be found here

References

During my research for this post I found some very helpful material from these sources, the first two of which relate to PdfSharp - which I didn't end up using, but were helpful none the lesss

https://stackoverflow.com/questions/54762881/ellipsis-or-wrapping-with-pdfsharp

https://www.youtube.com/watch?v=C-yMypr_TdY&t=329s

https://www.syncfusion.com/kb/9115/how-to-add-headers-and-footers-to-a-pdf-file