This article is compatible with the latest version of Silverlight.
Introduction
Some time ago Corey Schuman published the article A look at the Printing API in Silverlight right here on SilverlightShow.net. The article deals with the basics of printing in Silverlight, using a WYSIWYG approach. In this article we want to look into a more advanced scenario of printing, where WYSIWYG is not the best approach. We’ll take a look at how to print text in such a way that it will fit on the page and we’ll look into printing more than one page.
Before we start, a screenprint of what we’re building:
And if you want to have a look at the result, you can do so here. You can download the project here.
The concept
Before we dive into the code, let’s take a look at how we want our printing demo to work. First of all we want to print multiple pages with a header and a footer on each page. We also want our text to always fit on the page, instead of the text being formatted exactly like on screen. And finally we want our text to span multiple pages as needed.
In order to meet these requirements we need a separate user control that will be send to the printing API. The user control needs to provide a format containing the header, body and footer. In order to achieve a layout like that I chose to use a Grid with three rows. One thing to be aware of is that the Grid will automatically take up all the space on the page, which is exactly what we want. The reason it does this is that it is never displayed on screen, so it’s parent will actually be the page itself. Therefore the page will dictate the maximum allowed space for the grid.
Once we have that, we need to make sure that we only put just enough text on a page to fill it, not more and not less. To do this, we can use the layout system in Silverlight to make sure our content fits on the page. We’ll combine it with a trial-and-error approach to find the exact content to fit on the page. You can use the same concept for printing data on a page as well.
Step 1: Laying out the pages
Before we can print anything, we need to create a UserControl to hold the layout. I created a UserControl called PrintPage which does exactly that. The XAML code is kept simple to demonstrate the principles:
<Grid x:Name="documentRoot">
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition />
<RowDefinition Height="25"/>
</Grid.RowDefinitions>
<TextBlock x:Name="headerTextBlock" HorizontalAlignment="Center" />
<TextBlock x:Name="bodyTextBlock" Grid.Row="1" TextWrapping="Wrap" />
<TextBlock x:Name="footerTextBlock" HorizontalAlignment="Center" Grid.Row="2"/>
</Grid>
As you can see, there is a Grid with three rows of which the middle row will size based on it’s content. The middle row contains the TextBlock that will contain our body text. The top and bottom rows contain the TextBlocks for the header and footer.
There is also some simple code to allow us to fill the page:
public partial class PrintPage : UserControl
{
public PrintPage()
{
InitializeComponent();
}
public void SetHeaderAndFooterText(string header, string footer)
{
headerTextBlock.Text = header;
footerTextBlock.Text = footer;
}
public void AddLine(string line)
{
bodyTextBlock.Inlines.Add(line);
bodyTextBlock.Inlines.Add(new LineBreak());
}
public void RemoveLastLine()
{
for (int index = 0; index < 2; index++)
{
bodyTextBlock.Inlines.RemoveAt(bodyTextBlock.Inlines.Count - 1);
}
}
}
So we have a method to set both the header and footer for the page. More important are the methods to add and remove lines from the body. The code is pretty simple, but we’ll look into how they are used later on.
Step 2: Getting ready to print
Now that we have a PrintPage control, we are ready to use it for printing. I’ll spare you the details on building a test application around it and skip right to the good stuff. In the article by Corey Schuman you’ve already had an introduction to the PrintDocument class and the printing API. In order for us to print a document on multiple pages we need a more advanced use of the same components:
_lineIndex = 0;
_documentBodyLines = new List<string>();
string[] lines = bodyTextBox.Text.Split(new char[] { '\r' }, StringSplitOptions.None);
_documentBodyLines.AddRange(lines);
PrintDocument printDocument = new PrintDocument();
printDocument.BeginPrint += new EventHandler<BeginPrintEventArgs>(printDocument_BeginPrint);
printDocument.EndPrint += new EventHandler<EndPrintEventArgs>(printDocument_EndPrint);
printDocument.PrintPage += new EventHandler<PrintPageEventArgs>(printDocument_PrintPage);
printDocument.Print("SLPrintDemo document");
First we reset the _lineIndex field, which we will be needing during printing. Next we take the complete document text and prepare it for printing, by splitting it into separate lines. Next we setup the PrintDocument class we will be using for print. Besides attaching to the PrintPage event, we also attach handlers to BeginPrint and EndPrint so we can give some feedback to the user on when we are done printing. And finally we call Print to bring up the print dialog.
The reason I’ve not used an inline anonymous method is that the code in the PrintPage event handler is more then just a couple of lines. Putting the code inline here would make it unreadable.
Step 3: Printing a page
Now that we have setup our PrintDocument class we can focus on printing a single page. Note that we can’t tell what page we are printing from the PrintPage event handler. We can only tell what page we are printing from our own state. Let’s take a look at the code:
void printDocument_PrintPage(object sender, PrintPageEventArgs e)
{
PrintPage page = new PrintPage();
page.SetHeaderAndFooterText(headerTextBox.Text, footerTextBox.Text);
int numberOfLinesAdded = 0;
while (_lineIndex < _documentBodyLines.Count)
{
page.AddLine(_documentBodyLines[_lineIndex]);
page.Measure(new Size(e.PrintableArea.Width, double.PositiveInfinity));
if (page.DesiredSize.Height > e.PrintableArea.Height
&& numberOfLinesAdded > 1)
{
page.RemoveLastLine();
e.HasMorePages = true;
break;
}
_lineIndex++;
numberOfLinesAdded++;
}
e.PageVisual = page;
}
We obviously start by creating a PrintPage control to represent our page in the printing API and then we set the header and footer by calling the SetHeaderAndFooterText method. Next we initialize a variable called numberOfLinesAdded which will hold the number of lines we have added to the page. Then we start looping through the lines of the document, which we initialized before we started printing.
Inside the loop we start adding lines to the current page. Then we call the Measure method on the page provided by Silverlight. This will update the DesiredSize property of a UserControl. In our application it will adjust it according to the amount of text included in the body. Then we check if the height our page would be can fit into the PrintibleArea as it is provided by the printing API. If it doesn’t fit and we did add at least two lines, we will then remove the last line. Then we will tell the printing API we need to print another page by setting the HasMorePages property of the event arguments to true and break out of the loop. If we have not exceeded the available height, we will then move on the the next line.
As we exit the loop we then set the PageVisual property to represent our PrintPage control, which prints the page.
Conclusion
So here is what we’ve seen in the article:
- Making sure your content fits on the page is easy if you use the Silverlight layout system
- If you want your content to fit on the page, don’t use on screen controls
- The HasMorePages property of the PrintDocument class is your friend in these scenario’s