Monday 5 December 2011

Printing in WPF

The most stright forward tool for printing in WPF was written my Chris L Mullin http://www.codeproject.com/KB/WPF/CustomDataGridDocPag.aspx.
It is only one class that you add to your project, and start printing.
I have added two little things to the class:

- RightToLeft support to the class; and that made life much easier for me.
- Customize the Styles (added by Chris) using XAML code in the App.xaml.
But, full credit goes to Chris.

The whole idea is to add a DataGrid and customize the column widths, headers, ...etc. once done, add a button and paste few lines of code, that will create a paginator from the DataGrid and stright to the printer.

Steps to achieve our WPF printing goal:

Step #1
======

Add new Class to your WPF project and past the following:
///////////////////////////CodeStartHere//////////////////////
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Documents;
using System.Windows.Controls;
using System.Windows;
using System.Collections.ObjectModel;
using System.Collections;
using System.Windows.Markup;
using System.Windows.Data;
using System.ComponentModel;
namespace WPFPrintingExample
{
    public class CustomDataGridDocumentPaginator : DocumentPaginator
    {
        #region Private Members
        private DataGrid _documentSource;
        private Collection<ColumnDefinition> _tableColumnDefinitions;
        private double _avgRowHeight;
        private double _availableHeight;
        private int _rowsPerPage;
        private int _pageCount;
        #endregion
        #region Constructor
        public CustomDataGridDocumentPaginator(DataGrid documentSource, string documentTitle, Size pageSize, Thickness pageMargin)
        {
            _tableColumnDefinitions = new Collection<ColumnDefinition>();           
            _documentSource = documentSource;
            this.DocumentTitle = documentTitle;
            this.PageSize = pageSize;
            this.PageMargin = pageMargin;
            if (_documentSource != null)
                MeasureElements();
            this.PageDirection = documentSource.FlowDirection;
            ReadStyles();
        }
        private void ReadStyles()
        {
            this.DocumentHeaderTextStyle = (Style)App.Current.Resources["DocumentHeaderTextStyle"];
            this.AlternatingRowBorderStyle = (Style)App.Current.Resources["AlternatingRowBorderStyle"];
            this.DocumentFooterTextStyle = (Style)App.Current.Resources["DocumentFooterTextStyle"];
            this.TableCellTextStyle = (Style)App.Current.Resources["TableCellTextStyle"];
            this.TableHeaderTextStyle = (Style)App.Current.Resources["TableHeaderTextStyle"];
            this.TableHeaderBorderStyle = (Style)App.Current.Resources["TableHeaderBorderStyle"];
            this.GridContainerStyle = (Style)App.Current.Resources["GridContainerStyle"];
        }

        #endregion
        #region Public Properties
        #region Styling
        public Style AlternatingRowBorderStyle { get; set; }
        public Style DocumentHeaderTextStyle { get; set; }
        public Style DocumentFooterTextStyle { get; set; }
        public Style TableCellTextStyle { get; set; }
        public Style TableHeaderTextStyle { get; set; }
        public Style TableHeaderBorderStyle { get; set; }
        public Style GridContainerStyle { get; set; }
        #endregion
        public FlowDirection PageDirection { get; set; }
       
        public string DocumentTitle { get; set; }
        public Thickness PageMargin { get; set; }
        public override Size PageSize { get; set; }
        public override bool IsPageCountValid
        {
            get { return true; }
        }
        public override int PageCount
        {
            get { return _pageCount; }
        }
        public override IDocumentPaginatorSource Source
        {
            get { return null; }
        }
        #endregion
        #region Public Methods
        public override DocumentPage GetPage(int pageNumber)
        {
            DocumentPage page = null;
            List<object> itemsSource = new List<object>();
            ICollectionView viewSource = CollectionViewSource.GetDefaultView(_documentSource.ItemsSource);
            if (viewSource != null)
            {
                foreach (object item in viewSource)
                    itemsSource.Add(item);
            }
            if (itemsSource != null)
            {
                int rowIndex = 1;
                int startPos = pageNumber * _rowsPerPage;
                int endPos = startPos + _rowsPerPage;
                //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++;
                }
               
                tableGrid.FlowDirection = PageDirection;
                page = ConstructPage(tableGrid, pageNumber);
            }
            return page;
        }
        #endregion
        #region Private Methods
        /// <summary>
        /// This function measures the heights of the page header, page footer and grid header and the first row in the grid
        /// in order to work out how manage pages might be required.
        /// </summary>
        private void MeasureElements()
        {
            double allocatedSpace = 0;
            //Measure the page header
            ContentControl pageHeader = new ContentControl();
            pageHeader.Content = CreateDocumentHeader();
            allocatedSpace = MeasureHeight(pageHeader);
           
            //Measure the page footer
            ContentControl pageFooter = new ContentControl();
            pageFooter.Content = CreateDocumentFooter(0);
            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;
            //Work out how much space we need to display the grid
            _availableHeight = this.PageSize.Height - allocatedSpace;
            //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);
            //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));
        }
        /// <summary>
        /// This method constructs the document page (visual) to print
        /// </summary>
        private DocumentPage ConstructPage(Grid content, int pageNumber)
        {
            if (content == null)
                return null;
            //Build the page inc header and footer
            Grid pageGrid = new Grid();
            //Header row
            AddGridRow(pageGrid, GridLength.Auto);
            //Content row
            AddGridRow(pageGrid, new GridLength(1.0d, GridUnitType.Star));
            //Footer row
            AddGridRow(pageGrid, GridLength.Auto);
            ContentControl pageHeader = new ContentControl();
            pageHeader.Content = this.CreateDocumentHeader();
            pageGrid.Children.Add(pageHeader);
           
            if (content != null)
            {
                content.SetValue(Grid.RowProperty, 1);
                pageGrid.Children.Add(content);
            }
            ContentControl pageFooter = new ContentControl();
            pageFooter.Content = CreateDocumentFooter(pageNumber + 1);
            pageFooter.SetValue(Grid.RowProperty, 2);
            pageFooter.FlowDirection = PageDirection;
            pageGrid.Children.Add(pageFooter);
            double width = this.PageSize.Width - (this.PageMargin.Left + this.PageMargin.Right);
            double height = this.PageSize.Height - (this.PageMargin.Top + this.PageMargin.Bottom);
            pageGrid.Measure(new Size(width, height));
            pageGrid.Arrange(new Rect(this.PageMargin.Left, this.PageMargin.Top, width, height));
            return new DocumentPage(pageGrid);
        }
        /// <summary>
        /// Creates a default header for the document; containing the doc title
        /// </summary>
        private object CreateDocumentHeader()
        {
            Border headerBorder = new Border();
            TextBlock titleText = new TextBlock();
            titleText.Style = this.DocumentHeaderTextStyle;
            titleText.TextTrimming = TextTrimming.CharacterEllipsis;
            titleText.Text = this.DocumentTitle;
            titleText.HorizontalAlignment = HorizontalAlignment.Center;
            titleText.TextAlignment = TextAlignment.Center;
            headerBorder.Child = titleText;
            headerBorder.FlowDirection = PageDirection;
            return headerBorder;
        }
        /// <summary>
        /// Creates a default page footer consisting of datetime and page number
        /// </summary>
        private object CreateDocumentFooter(int pageNumber)
        {
            Grid footerGrid = new Grid();
            footerGrid.Margin = new Thickness(0, 10, 0, 0);
            ColumnDefinition colDefinition = new ColumnDefinition();
            colDefinition.Width = new GridLength(0.5d, GridUnitType.Star);
            TextBlock dateTimeText = new TextBlock();
            dateTimeText.Style = this.DocumentFooterTextStyle;
            switch (PageDirection)
            {
                case FlowDirection.LeftToRight:
                   dateTimeText.Text = DateTime.Now.ToString("dd-MMM-yyy HH:mm");
                   break;
                case FlowDirection.RightToLeft:
                   dateTimeText.Text = DateTime.Now.ToString("yyyy-MM-dd");
                   break;
            }
            dateTimeText.FlowDirection = PageDirection;
            footerGrid.Children.Add(dateTimeText);
            TextBlock pageNumberText = new TextBlock();
            pageNumberText.Style = this.DocumentFooterTextStyle;
            switch (PageDirection)
            {
                case FlowDirection.LeftToRight:
                    pageNumberText.Text = string.Format("Page {0} of {1}",pageNumber.ToString(),this.PageCount.ToString());
                    break;
                case FlowDirection.RightToLeft:
                    pageNumberText.Text = string.Format("صفحة {0} من {1}", pageNumber.ToString(), this.PageCount.ToString());                   
                    break;
            }
            pageNumberText.HorizontalAlignment = HorizontalAlignment.Right;
            pageNumberText.FlowDirection = PageDirection;
            pageNumberText.SetValue(Grid.ColumnProperty, 1);
            footerGrid.Children.Add(pageNumberText);
            return footerGrid;
        }
        /// <summary>
        /// Counts the number of rows in the document source
        /// </summary>
        /// <param name="itemsSource"></param>
        /// <returns></returns>
        private double CountRows(IEnumerable itemsSource)
        {
            int count = 0;
            if (itemsSource != null)
            {
                foreach (object item in itemsSource)
                    count++;
            }
            return count;
        }
        /// <summary>
        /// The following function creates a temp table with a single row so that it can be measured and used to
        /// calculate the totla number of req'd pages
        /// </summary>
        /// <returns></returns>
        private Grid CreateTempRow()
        {
            Grid tableRow = new Grid();
            if (_documentSource != null)
            {
                foreach (ColumnDefinition colDefinition in _tableColumnDefinitions)
                {
                    ColumnDefinition copy = XamlReader.Parse(XamlWriter.Save(colDefinition)) as ColumnDefinition;
                    tableRow.ColumnDefinitions.Add(copy);
                }
                foreach (object item in _documentSource.ItemsSource)
                {
                    int columnIndex = 0;
                    if (_documentSource.Columns != null)
                    {
                        foreach (DataGridColumn column in _documentSource.Columns)
                        {
                            if (column.Visibility == Visibility.Visible)
                            {
                                AddTableCell(tableRow, column, item, columnIndex, 0);
                                columnIndex++;
                            }
                        }
                    }
                    //We only want to measure teh first row
                    break;
                }
            }
            return tableRow;
        }
        /// <summary>
        /// This function counts the number of rows in the document
        /// </summary>
        private object CreateTable(bool createRowDefinitions)
        {
            if (_documentSource == null)
                return null;
            Grid table = new Grid();
            table.Style = this.GridContainerStyle;
            int columnIndex = 0;

            if (_documentSource.Columns != null)
            {
                double  totalColumnWidth = _documentSource.Columns.Sum(column => column.Visibility == Visibility.Visible ? column.Width.Value : 0);
                foreach (DataGridColumn column in _documentSource.Columns)
                {
                    if (column.Visibility == Visibility.Visible)
                    {
                        AddTableColumn(table, totalColumnWidth, columnIndex, column);
                        columnIndex++;
                    }
                }
            }
            if (this.TableHeaderBorderStyle != null)
            {
                Border headerBackground = new Border();
                headerBackground.Style = this.TableHeaderBorderStyle;
                headerBackground.SetValue(Grid.ColumnSpanProperty, columnIndex);
                headerBackground.SetValue(Grid.ZIndexProperty, -1);
                table.Children.Add(headerBackground);
            }
            if (createRowDefinitions)
            {
                for (int i = 0; i <= _rowsPerPage; i++)
                    table.RowDefinitions.Add(new RowDefinition());
            }
            return table;
        }
        /// <summary>
        /// Measures the height of an element
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        private double MeasureHeight(FrameworkElement element)
        {
            if (element == null)
                throw new ArgumentNullException("element");
            element.Measure(this.PageSize);
            return element.DesiredSize.Height;
        }
        /// <summary>
        /// Adds a column to a grid
        /// </summary>
        /// <param name="grid">Grid to add the column to</param>
        /// <param name="columnIndex">Index of the column</param>
        /// <param name="column">Source column defintition which will be used to calculate the width of the column</param>
        private void AddTableColumn(Grid grid, double totalColumnWidth, int columnIndex, DataGridColumn column)
        {
            double proportion = column.Width.Value / (this.PageSize.Width - (this.PageMargin.Left + this.PageMargin.Right));
            ColumnDefinition colDefinition = new ColumnDefinition();
            colDefinition.Width = new GridLength(proportion, GridUnitType.Star);
            grid.ColumnDefinitions.Add(colDefinition);
            TextBlock text = new TextBlock();
            text.Style = this.TableHeaderTextStyle;
            text.TextTrimming = TextTrimming.CharacterEllipsis;
            text.Text = column.Header.ToString();
            text.SetValue(Grid.ColumnProperty, columnIndex);
            grid.Children.Add(text);
            _tableColumnDefinitions.Add(colDefinition);
        }
        /// <summary>
        /// Adds a cell to a grid
        /// </summary>
        /// <param name="grid">Grid to add teh cell to</param>
        /// <param name="column">Source column definition which contains binding info</param>
        /// <param name="item">The binding source</param>
        /// <param name="columnIndex">Column index</param>
        /// <param name="rowIndex">Row index</param>
        private void AddTableCell(Grid grid, DataGridColumn column, object item, int columnIndex, int rowIndex)
        {           
            if (column is DataGridTemplateColumn)
            {
                DataGridTemplateColumn templateColumn = column as DataGridTemplateColumn;
                ContentControl contentControl = new ContentControl();
                contentControl.Focusable = true;
                contentControl.ContentTemplate = templateColumn.CellTemplate;
                contentControl.Content = item;
                contentControl.SetValue(Grid.ColumnProperty, columnIndex);
                contentControl.SetValue(Grid.RowProperty, rowIndex);
                grid.Children.Add(contentControl);
            }
            else if (column is DataGridTextColumn)
            {
                DataGridTextColumn textColumn = column as DataGridTextColumn;
                TextBlock text = new TextBlock { Text = "Text" };
                text.Style = this.TableCellTextStyle;
                text.TextTrimming = TextTrimming.CharacterEllipsis;
                text.DataContext = item;
                Binding binding = textColumn.Binding as Binding;
                //if (!string.IsNullOrEmpty(column.DisplayFormat))
                    //binding.StringFormat = column.DisplayFormat;
                text.SetBinding(TextBlock.TextProperty, binding);
                text.SetValue(Grid.ColumnProperty, columnIndex);
                text.SetValue(Grid.RowProperty, rowIndex);
                grid.Children.Add(text);
            }
        }
        /// <summary>
        /// Adds a row to a grid
        /// </summary>
        private void AddGridRow(Grid grid, GridLength rowHeight)
        {
            if (grid == null)
                return;
            RowDefinition rowDef = new RowDefinition();
            if (rowHeight != null)
                rowDef.Height = rowHeight;
            grid.RowDefinitions.Add(rowDef);
        }
        #endregion
    }
}
///////////////////////////CodeEndsHere//////////////////////
Do not forget to change the namespace to your project name, mine was: WPFPrintingExample
Step #2
======
Open the App.xaml found in your WPF project and paste the following between the  <Application.Resources> and  </Application.Resources> tags.
<!--XAML Code Starts Here-->
        <Style x:Key="DocumentHeaderTextStyle" TargetType="TextBlock">
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="Foreground" Value="Black"/>
            <Setter Property="FontFamily" Value="Aria"/>
            <Setter Property="FontSize" Value="30"/>
        </Style>
        <Style x:Key="AlternatingRowBorderStyle" TargetType="Border">
            <Setter Property="BorderBrush" Value="#FFFFFFFF"/>
            <Setter Property="Background" Value="#FFFFFFFF" />
        </Style>
       
        <Style x:Key="DocumentFooterTextStyle" TargetType="TextBlock">
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="Foreground" Value="Black"/>
            <Setter Property="FontFamily" Value="Times New Roman"/>
            <Setter Property="FontSize" Value="12"/>
        </Style>
        <Style x:Key="TableCellTextStyle" TargetType="TextBlock">
            <Setter Property="Padding" Value="5,0,0,0" />
        </Style>
        <Style x:Key="TableHeaderTextStyle" TargetType="TextBlock">
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="Background" Value="Black" />
            <Setter Property="FontFamily" Value="Times New Roman"/>
            <Setter Property="FontSize" Value="14"/>
            <Setter Property="TextAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style x:Key="TableHeaderBorderStyle" TargetType="Border">
            <Setter Property="BorderBrush" Value="Red"/>
            <Setter Property="Background" Value="Red" />
        </Style>
        <Style x:Key="GridContainerStyle" TargetType="Grid">
            <Setter Property="Background" Value="#FFFCFCFC"/>
        </Style>
<!--XAML Code Ends Here-->
This XAML code is an easy way to customize your WPF report.
Step #3
======
Add a DataGrid name it dgPringData to a new WPF window and fill it with data.
Add new button name it btnPrint and add the fllowing code to the click event:
///////////////////////////CodeStartHere//////////////////////
         private void btnPrint_Click(object sender, RoutedEventArgs e)
        {
            PrintDialog printDialog = new PrintDialog();
            if (printDialog.ShowDialog() == false)
                return;
            string documentTitle = "WPF Report Title";
            Size pageSize = new Size(printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight);
            CustomDataGridDocumentPaginator paginator = new CustomDataGridDocumentPaginator(dgPringData as DataGrid, documentTitle, pageSize, new Thickness(30, 20, 30, 20));
            printDialog.PrintDocument(paginator, "Grid");
        }
///////////////////////////CodeEndsHere//////////////////////

Source: Chris L Mullin