Introduction

Ever needed to print a datagrid or a list with repeating table headers on each page? How about page headers and footers containing the document title and page numbers? Well, this article describes how to do just that by creating your own custom Document Paginator.

Background

I recently had the task of having to extend our custom datagrid component to allow users to print the contents of the grid as a document, with repeating table headers as well as page headers containing the document title, and footers containing the page number and current date/time.

Having not written any printing type functionality before in WPF, I thought it would be a doddle in that you'd simply create a page definition using DataTemplates, which you'd set against some form of printing type component. How wrong was I - the reality was that you have a choice of either a FlowDocument or FixedDocument, neither of which provide you with the ability to do either of these things straight out-of-the-box.

So I began to do some investigation, and what I discovered was that in order to get around this problem, you have to create your own custom Document Paginator which, in the end, is what I did. However, one big difference between the solution that I came up with and others that I'd seen out there on the internet is that I do not use either FlowDocument or FixedDocument as the constructs behind the content of my documents.

Instead, after discovering that you can print almost any WPF visual wrapped inside a DocumentPage, I decided to manually generate my document using standard WPF controls, such as the Grid, TextBlock, and Border controls.

So how does it work?

It works by inheriting from the System.Windows.Documents.DocumentPaginator class, which is an abstract class that allows you to create multiple page elements from a single document source, which in our case is a DataGrid.

Within our custom document paginator, we work out how much space is available to display the contents of the grid. We do this by measuring the known elements, which are the page header, footer, and table header. When we add the heights of all of these elements together and subtract any margins, we get the total allocated space; see below:

The available space comes by subtracting the allocated space away from the page height:

//Work out how much space we need to display the grid
_availableHeight = this.PageSize.Height - allocatedSpace;

The next step is to then work out how many rows can fit on each page within the available space. We do this by measuring the height of the first row:

//Calculate the height of the first row
_avgRowHeight = MeasureHeight(CreateTempRow());
//Calculate how many rows we can fit on each page
double rowsPerPage = Math.Floor(_availableHeight / _avgRowHeight);
if (!double.IsInfinity(rowsPerPage))
_rowsPerPage = Convert.ToInt32(rowsPerPage);

Finally, we finish off the measuring part of the process by working out how many pages will be needed by dividing the total row count by the number of rows we can fit onto each page:

//Count the rows in the document source
double rowCount = CountRows(_documentSource.ItemsSource);
//Calculate the nuber of pages that we will need
if (rowCount > 0)
_pageCount = Convert.ToInt32(Math.Ceiling(rowCount / rowsPerPage));

The next step in the process is when we pass our custom document paginator to the PrintDialog via the PrintDocument method. What happens is the PrintDialog will call the all-important GetPage method on the paginator. This, ladies and gentlemen, is where the magic happens!

The GetPage method constructs a visual which it returns inside a new instance of a DocumentPage. Using the page number parameter that is passed into our overridden GetPage method, we work out which rows should go into the requested page. We do this by determining the start and end positions, like so:

Now that we have our content, we can generate the DocumentPage by calling the ConstuctPage method with the document contents as a parameter. It is this method that generates the document by building up a container grid which includes a document header, the document contents, and document footer.

And voila!

Final note: You will notice that in the example provided, there are several Style properties on our custom paginator. These allow you to style elements such as the individual table cells, the table header, page header, and document footers. You could even go a stage further by adding your own ControlTemplate properties that allow you to define exactly how you want the page headers and footers to look. Unfortunately, due to time constraints, I was unable to do this myself, but might do so in the future.

Conclusion

If you feel that the FixedDocument and FlowDocument classes don't give you enough power in terms of document layout and styling, then there is an alternative, and that is to create your own document paginator in which you generate your own content using standard WPF controls.

This is an excellent solution, but I did run into a few small bugs with it. Fortunately, they are easily fixed.

The first is in the ConstructPage() method. When it returns the "new DocumentPage(pageGrid)", that does not handle if the page size is set for landscape. So while it will scale the returned page for landscape, it does not actually set the page properties to landscape size, so the actual printout is cropped. This is easily fixed by changing the return to "new DocumentPage(pageGrid, PageSize, new Rect(content.DesiredSize), new Rect(content.DesiredSize))"

The second issue I ran into was if columns were resized prior to printing. The posted code results in only the resized columns being printed, and all other columns being set to a column width of 1. This is also an easy fix. In the AddTableColumn() method, in the first line where it is setting up the "proportion" local variable, change the "column.Width.Value" to "column.ActualWidth".

If _rowsPerPage is creater than actual count of rows in the datagrid, it will print empty lines
to the end of a page. With "normal" letter/A4 that´s ok but with POS printers(receipt pinters), page height
can be about 2 meters (more than 12000mm) which means that one page can contain 769 rows(in your example).
So, if you want to print lets say 70 rows, it will print whole 2 meters which isn´t desirable result.

My datagrid also contained checkboxes I wanted to have showing up, I've added this piece of code to the "AddTableCell" function to get this to work. Maybe handy for other people who need this or something with similar control.

Thanks for excellent solution. I have noticed that if CellTemplate column having ComboBox is printed, the print does not display the content with the combobox. Have you come across this issue, is there any solution?

Great article Chris! However, if you were to enlarge the first column; let's say, big enough to span to two pages. With your implementation, the rest of the columns to the right will not be printed at all. It looks like you're only handling pages based on the growth of the rows.

We could not break the control into Multiple Pages... Not only in WPF.. for eg look into MS-Word..if i am having an Image size larger than my page size..i could not see the image into next page... but u can change the way of developing your application..

I mean you can use Tables Instead of DataGrid Control..

i had the same Experience while Developing the Reporting Application..