Introduction
A mail merge involves merging the records in a database with a document template to produce a set of similar documents where the body of the document is constant but the details, specific to each individual record, change. This article explorers how to use WPF’s document viewing and printing classes to inspect, scale, and output mail merge letters produced from a XAML based template.
Demo Application
In the demo, the template takes the form of a letter to be sent to clients of a Nature Reserve. There is a facility to preview the letters to check that they are formatted correctly and the ability to print in batches so that the whole print run is not lost when the printer decides to trash some paper. All the functionality required is provided by WPF’s document viewing and printing classes; the demo’s code is mainly concerned with interfacing the classes.
Mail Merge Letter Template
XAML, with its many layout items, is ideal for producing the template. The demo uses a UserControl
with a DockPanel
. The letter’s header and footer TextBlock
items are docked to the top and bottom, respectively, with the letter body consisting of more TextBlock
s stacked within a StackPanel
. Databinding binds the relevant TextBlock
s to properties in the ViewModel. The ViewModel, in turn, is populated by individual records from the data set.
<UserControl x:Class="printing.MailMergeLetter"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Height="1104" Width="792">
<UserControl.Resources>
<Style TargetType="TextBlock" x:Key="BodyText">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="Times New Roman"/>
<Setter Property="FontSize" Value="15"/>
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</UserControl.Resources>
<DockPanel>
<TextBlock
DockPanel.Dock="Top"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Height="30"
Margin="0,48,0,48"
FontFamily="Times New Roman" FontSize="32" >
<Bold>Hawk's Nature Reserve</Bold>
</TextBlock>
<TextBlock
Style="{StaticResource BodyText}"
DockPanel.Dock="Bottom"
HorizontalAlignment="Center"
Margin="0,0,0,60"
Name="footer"
>
<Bold>23 Park Meadows Pembroke PB44 7BN Tel 016583946</Bold>
</TextBlock>
<TextBlock
Style="{StaticResource BodyText}"
DockPanel.Dock="Bottom"
Margin="96,0,0,192"
Text="A. Sparrow-Hawk"
/>
<StackPanel>
<TextBlock
Style="{StaticResource BodyText}"
FontWeight="Bold"
Margin="96,20,96,45"
Text="{Binding Path=Address}"
/>
<TextBlock
Style="{StaticResource BodyText}"
Margin="96,25,96,45"
Text="{Binding Path=Date}"
/>
<TextBlock
Style="{StaticResource BodyText}"
Margin="96,10,96,0"
Text="{Binding Path=Salutation}"
/>
<TextBlock
Style="{StaticResource BodyText}"
Margin="96,10,96,0"
Text="{Binding Path=Body}"
/>
<TextBlock
Style="{StaticResource BodyText}"
Margin="96,10,96,0"
Name="validation"
Text="Your feathered friend,"
/>
</StackPanel>
</DockPanel>
</UserControl>
Constructing the Document
Viewing and printing various types of documentation in WPF is centered around the Document
class. By adding the mail merge letters to an object of type Document
, all the functionality it provides becomes available to the mail merge. In the demo, the mail merge letters are added to a FixedDocument
object and form the individual pages of the Document
. A FixedDocument
is used as it is designed to hold Visual
(UI) elements. The FlowDocument
, the other main document type, is not as suitable as it is geared more towards rendering blocks of text stored in XML format.
Adding the mail-merge letters to a Document
is straightforward.
FixedDocument fixedDocument = new FixedDocument();
MailMergeLetter mailMergeLetter;
for (int i = 0; i < data.Length; i++)
{
mailMergeLetter = documentBuilder.BuildPage(data, i);
PageContent pageContent = new PageContent();
FixedPage fixedPage = documentBuilder.CreatePage();
fixedPage.Children.Add(mailMergeLetter);
((IAddChild)pageContent).AddChild(fixedPage);
fixedDocument.Pages.Add(pageContent);
}
this.Document = fixedDocument;
Viewing the Document
Viewing the Document
is just a matter of using the DocumentViewer
control and binding its Document
property to the mail merge Document
. It is possible to print out the letters using default settings but, to determine how the letters are printed, it's necessary to get control of the DocumentViewer
’s OnPrint
command. There are two main ways of doing this. One is to replace the control’s template and the other is to subclass the DocumentViewer
and override its OnPrint
method. The latter approach is used in the demo.
protected override void OnPrintCommand()
{
PrintDialog printDialog = new PrintDialog();
printDialog.UserPageRangeEnabled = true;
printDialog.PrintTicket = printDialog.PrintQueue.UserPrintTicket;
printDialog.PrintTicket.PageOrientation = PageOrientation.Portrait;
printDialog.PrintQueue = LocalPrintServer.GetDefaultPrintQueue();
if (printDialog.ShowDialog() == true)
{
this.PerformPrintRun(printDialog);
}
}
The PrintDialog
control allows the user to configure the dialog's PrintTicket
class. The PrintTicket
contains information about how the printing is to be carried out. The options available for the PrintTicket
depend upon the capabilities of the selected printer. It's not necessary to interrogate the printer to determine its properties as this is neatly done by the PrintDialog
.
Printing the Mail Merge
The FixedDocument
has a DocumentPaginator
object. One of the DocumentPaginator
’s tasks is to present individual pages from the Document
to the printer for printing. This is done using the paginator’s GetPage
method. But, as it stands, the GetPage
method does not have the ability to print a selected range of pages. So, to use the DocumentPaginator
for printing a range of pages, it's necessary to subclass the DocumentPaginator
and override the GetPage
method.
public override DocumentPage GetPage(int pageNumber)
{
PageContent pageContent =
this.fixedDocument.Pages[pageNumber + this.startIndex];
return new DocumentPage(pageContent, this.paperSize,
new Rect(this.paperSize), new Rect(this.paperSize));
}
The number of calls to GetPage
is determined by the Paginator
's PageCount
property. A range of pages can be printed by setting PageCount
to the appropriate value and summing the pageNumber
with the StartIndex
obtained from the page range selection of the PrintDialog
control. But there is a potential problem with this technique. Once a Visual
becomes part of a Document
's Pages
collection, it becomes inextricably bound to the collection, and trying to associate it with another element can throw an exception along the lines of "Specified element is already the logical child of another element. Disconnect it first" - only you can't disconnect it as the Parent
property is read only. This exception is somewhat idiosyncratic. Its appearance seems to depend upon the Operating Ssystem and the print driver.
Fortunately, there is an alternative method of printing that does not use a DocumentPaginator
but creates a VisualsToXpsDocument
to write the MailMerge
Visual
d directly to the PrintQueue
in batch mode. This can be called from the DocumentViewer
's OnPrint
method, and is best used with a FixedDocument
where the pages are of a predefined fixed size and the Paginator
's function is limited.
XpsDocumentWriter writer =
PrintQueue.CreateXpsDocumentWriter(printDialog.PrintQueue);
VisualsToXpsDocument visualsToXpsDoc =
(VisualsToXpsDocument)writer.CreateVisualsCollator();
Size scaling = this.GetScalingFactor();
Size paperSize = this.GetPaperSize();
visualsToXpsDoc.BeginBatchWrite();
for (int i = startIndex; i <= endIndex; i++)
{
PageContent pageContent = this.fixedDocument.Pages[i];
pageContent.Child.LayoutTransform =
new ScaleTransform(scaling.Width, scaling.Height);
pageContent.Child.Measure(paperSize);
pageContent.Child.Arrange(new Rect(paperSize));
visualsToXpsDoc.Write(pageContent);
}
visualsToXpsDoc.EndBatchWrite();
The PrintDialog
control often allows the user to select a paper size, which means that it may be necessary to scale the MailMerge
to fit the paper. To accommodate scaling, pageContent.Child.LayoutTransform
is set to the appropriate scaling factor. Then the amount of space requested for each element is determined by calling Measure
and, finally, the layout is updated by calling the Arrange
method.
Conclusion
The methods described here are not the most efficient way to print documents. But they do allow scaling to different paper sizes and a print range selection to be made.
Acknowledgements
I’d like to express my gratitude to CodeProject’s regular contributors. They have taught me most of what I know. Any inadequacies in this article are entirely due to my own short comings and not theirs.