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:
double allocatedSpace = 0;
ContentControl pageHeader = new ContentControl();
pageHeader.Content = pageHeader;
allocatedSpace = MeasureHeight(pageHeader);
ContentControl pageFooter = new ContentControl();
pageFooter.Content = pageFooter;
allocatedSpace += MeasureHeight(pageFooter);
ContentControl tableHeader = new ContentControl();
tableHeader.Content = CreateTable(false);
allocatedSpace += MeasureHeight(tableHeader);
allocatedSpace += this.PageMargin.Bottom + this.PageMargin.Top;
The available space comes by subtracting the allocated space away from the page height:
_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:
_avgRowHeight = MeasureHeight(CreateTempRow());
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:
double rowCount = CountRows(_documentSource.ItemsSource);
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:
int startPos = pageNumber * _rowsPerPage;
int endPos = startPos + _rowsPerPage;
Once we know the start and end positions, we can then create a new table (Grid
) by iterating though the document source:
Grid tableGrid = CreateTable(true) as Grid;
for (int index = startPos; index < endPos &&
index < itemsSource.Count; index++)
{
Console.WriteLine("Adding: " + index);
if (rowIndex > 0)
{
object item = itemsSource[index];
int columnIndex = 0;
if (_documentSource.Columns != null)
{
foreach (DataGridColumn column in _documentSource.Columns)
{
if (column.Visibility == Visibility.Visible)
{
AddTableCell(tableGrid, column, item, columnIndex, rowIndex);
columnIndex++;
}
}
}
if (this.AlternatingRowBorderStyle != null && rowIndex % 2 == 0)
{
Border alernatingRowBorder = new Border();
alernatingRowBorder.Style = this.AlternatingRowBorderStyle;
alernatingRowBorder.SetValue(Grid.RowProperty, rowIndex);
alernatingRowBorder.SetValue(Grid.ColumnSpanProperty, columnIndex);
alernatingRowBorder.SetValue(Grid.ZIndexProperty, -1);
tableGrid.Children.Add(alernatingRowBorder);
}
}
rowIndex++;
}
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.
Happy coding!