Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Custom Data Grid Document Paginator

0.00/5 (No votes)
20 Dec 2010 2  
This article describes how to create your own custom data grid document paginator.

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;

//Measure the page header
ContentControl pageHeader = new ContentControl();
pageHeader.Content = pageHeader;
allocatedSpace = MeasureHeight(pageHeader);
            
//Measure the page footer
ContentControl pageFooter = new ContentControl();
pageFooter.Content = pageFooter;
allocatedSpace += MeasureHeight(pageFooter);
            
//Measure the table header
ContentControl tableHeader = new ContentControl();
tableHeader.Content = CreateTable(false);
allocatedSpace += MeasureHeight(tableHeader);

//Include any margins
allocatedSpace += this.PageMargin.Bottom + this.PageMargin.Top;

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:

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:

//Create a new grid
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!

DataGridDocumentPaginator.png

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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here