Printing data on multiple pages with Silverlight 4

This week at PDC09 Microsoft unveiled latest Silverlight release – first beta of version 4. One of the new features is printing support. There is a great video introduction to printing published by Tim Heuer. It show the basics, after watching it my first question was how to print the data on multiple pages. Here is how I do it. This application is a cut down version of the app published by Tim on silverlight.net.

In this sample we are going to print list of people and display sample header showing page number and some random title. Let’s jump in.

Let’s start with XAML:

<UserControl x:Class="PrintingTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    <Grid x:Name="LayoutRoot" Background="White">
        <data:DataGrid
            ItemsSource="{Binding}"
            Height="247" HorizontalAlignment="Left"
            Margin="12,41,0,0" Name="dataGrid1"
            VerticalAlignment="Top" Width="376" />
        <Button
            Content="Print data" Height="23"
            HorizontalAlignment="Left" Margin="12,12,0,0"
            Name="PrintDataBtn" VerticalAlignment="Top"
            Width="126" Click="PrintDataBtn_Click" />
    </Grid>
</UserControl>

Next is the Person class which is exactly the same as in the original video:

using System;

namespace PrintingTest
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
        public string FullName {
            get {
                return string.Concat(FirstName, " ", LastName);
            }
        }
    }
}

And now the most important part – the code behind. The MainPage class wires up the Loaded event and when the app is loaded it populates the list of people as well as sets the DataContext on LayoutRoot.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Printing;
using System.Windows.Media;

namespace PrintingTest
{
    public partial class MainPage : UserControl
    {

        public MainPage()
        {
            InitializeComponent();
            Loaded += new RoutedEventHandler(MainPage_Loaded);
        }

        void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            List<Person> people = new List<Person>();
            for (int i = 0; i < 20; i++)
            {
                people.Add(new Person() { FirstName = "Al", LastName = "Pacino", Age = 69 });
                people.Add(new Person() { FirstName = "Robert", LastName = "De Niro", Age = 66 });
                people.Add(new Person() { FirstName = "Val", LastName = "Kilmer", Age = 50 });
                people.Add(new Person() { FirstName = "John", LastName = "Voight", Age = 71 });
                people.Add(new Person() { FirstName = "Tom", LastName = "Sizemore", Age = 48 });
            }
            LayoutRoot.DataContext = people;
        }

        private void PrintDataBtn_Click(object sender, RoutedEventArgs e)
        {
            // header height:
            double headerHeight = 70.0;
            // footer height:
            double footerHeight = 30.0;
            // single line height:
            double tboxHeight = 20.0;
            // how many elements can we place on one page?
            int numElements = -1;
            // current page number:
            int page = 1;
            // total number of pages to print:
            int totalPages = 0;
            // number of the current item:
            int counter = 1;

            PrintDocument doc = new PrintDocument();
            doc.PrintPage += (s, args) =>
            {
            };
           doc.Print();
        }

    }
}

The code below is responsible for printing and should be placed inside doc.PrintPage lambda. Let’s discuss what is going on there first.

When the application attempts to compose first page the number of elements per page and total number of pages is calculated. Next, the header is created. It is a Grid with two TextBlocks. First of the text blocks holds the title while the second one displays current and the total pages numbers; header is created separately for each page.

Once the header is constructed the dynamicPanel StackPanel is created and previously constructed headerGrid is added to its Children list. Next, we query dataGrid1.ItemsSource for the first of people to display on current page. We skip as many elements as we fitted on previous pages and take number of elements for the current one.

The penultimate step is enumerating over ppl list and for each one of them create a StackPanel with two TextBlocks. First displays the name of the person, second one the age. Finally we set HasMorePages.

HasMorePages is the key to multiple pages printing. As long as it is set to true Silverlight expects more pages to be printed. That’s why when the number of current page is equal to total number of pages we set it to false. At this point printing the document is finished.

Here is the code:

                if (page == 1)
                {
                    numElements = (int)((args.PrintableArea.Height - headerHeight - footerHeight) / tboxHeight);
                    double totalItems = (double)((List<Person>)dataGrid1.ItemsSource).Count;
                    totalPages = (int)Math.Ceiling(totalItems / (double)numElements);
                }

                Grid headerGrid = new Grid()
                    {
                        Width = args.PrintableArea.Width,
                        Height = headerHeight
                    };
                headerGrid.RowDefinitions.Add(new RowDefinition());
                headerGrid.ColumnDefinitions.Add(new ColumnDefinition());
                headerGrid.ColumnDefinitions.Add(new ColumnDefinition());

                TextBlock headerTitle = new TextBlock()
                {
                    Text = "Contact list",
                    HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
                    VerticalAlignment = System.Windows.VerticalAlignment.Center,
                    Margin = new Thickness(50,0,0,0),
                    FontSize = 20
                };
                TextBlock pageCounter = new TextBlock()
                {
                    Text = string.Concat("Page ", page, " of ", totalPages),
                    HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
                    VerticalAlignment = System.Windows.VerticalAlignment.Center,
                    Margin = new Thickness(0, 0, 50, 0)
                };
                headerGrid.Children.Add(headerTitle);
                headerGrid.Children.Add(pageCounter);
                Grid.SetColumn(headerTitle, 1);
                Grid.SetColumn(pageCounter, 2);

                StackPanel dynamicPanel = new StackPanel();
                // add header to display list:
                dynamicPanel.Children.Add(headerGrid);

                IEnumerable<Person> ppl = ((List<Person>)dataGrid1.ItemsSource).Skip((page - 1) * numElements).Take(numElements);

                foreach (Person p in ppl)
                {
                    // create stack panel for each person:
                    StackPanel namePanel = new StackPanel();
                    namePanel.Orientation = Orientation.Horizontal;

                    TextBlock counterBlock = new TextBlock();
                    counterBlock.TextAlignment = TextAlignment.Right;
                    counterBlock.Margin = new Thickness(0, 0, 5, 0);
                    counterBlock.Text = string.Concat(counter, ".");
                    counterBlock.Height = tboxHeight;
                    counterBlock.Width = 50.0;

                    TextBlock nameBlock = new TextBlock();
                    nameBlock.Text = p.FullName;
                    nameBlock.Height = tboxHeight;

                    TextBlock ageBlock = new TextBlock();
                    ageBlock.Text = string.Concat("who is ", p.Age, " years old");
                    ageBlock.Foreground = new SolidColorBrush(Colors.Red);
                    ageBlock.Margin = new Thickness(5, 0, 0 , 0);
                    ageBlock.Height = tboxHeight;

                    namePanel.Children.Add(counterBlock);
                    namePanel.Children.Add(nameBlock);
                    namePanel.Children.Add(ageBlock);
                    dynamicPanel.Children.Add(namePanel);

                    counter++;
                }

                args.HasMorePages = !(page == totalPages);
                args.PageVisual = dynamicPanel;

                page++;

Full source code can be downloaded from here. You can test working example below, please make sure you use Silverlight 4 beta. It can be installed from this location.

Install Microsoft Silverlight

Sample PDF output can be seen here.

4 Responses to “Printing data on multiple pages with Silverlight 4”

  1. Mike Says:

    Hi,

    Great stuff :-)

    I think my little example of printing images up here http://mtaulty.com/CommunityServer/blogs/mike_taultys_blog/archive/2009/11/18/silverlight-4-rough-notes-printing.aspx also paginates in a simple way as a 2nd example.

    Mike.

  2. Sam Says:

    Can you print without Print dialog box?
    Scenario – Page loaded, you press print button, pagegoes to default printer without any dialog boxes.

  3. radekg Says:

    @Sam: I don’t think it is and should be possible. Remember that this is working on the client and the user should always be in full control on what’s going on. This document http://msdn.microsoft.com/en-us/library/ee671023(VS.96).aspx says that the StartPrint callback is actually executed once the Print button in the Print dialog has been clicked so “all” what’s Print() method is doing is showing the print popup.

  4. Austin Naillon Says:

    Rather nice article, very helpful stuff. Never ever considered I would discover the information I would like right here. I’ve been scouring everywhere in the internet for a while now and had been starting to get disappointed. Fortunately, I stumbled across your internet site and acquired precisely what I was browsing for.

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>