My first WPF application is near completion, but one of the last items on the list is report printing. After a couple of hours on the internet I really did not see an ideal solution. There were however some aspects of the solutions I saw that appealed so I decided that I would pick out the best bits of what I had found and script my own Report Writer.
One example that got my attention on the internet was Azam Sharp. I thought his Flow Document example was simple and clean solution. What I wanted to do was take Azam’s example to the next level and give myself a tool to quickly design a report for my new project.
My Requirement list went like this.
- Print Preview
- Page Header
- Page Footer
- Full control of the report layout
- Keep it simple.
I will demonstrate my solution using a similar Business Entity as Azam did.
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
// I'll add another column as one should always try and improve a copied solution <g>.
public string Phone { get; set; }
}
My Customers Class will mange the Customer Collection
public class Customers
{
List<Customer> _customers = new List<Customer>();
public Customers()
{
for (int i = 1; i <= 80; i++)
{
Customer customer = new Customer();
customer.FirstName = "FirstName " + i;
customer.LastName = "LastName " + i;
customer.Phone = "Phone " + i;
_customers.Add(customer);
}
}
public List<Customer> FullCustomerList { get { return _customers; } }
public int CustomerCount { get { return _customers.Count(); } }
public Customer[] GetReportLineItems(int skipCount, int takeCount)
{
var list = (from c in _customers
select c).Skip(skipCount).Take(takeCount);
return list.ToArray(); ;
}
}
The operator will select and confirm the printing properties from the standard Print Dialog. I have designed a small class to record the values from the print dialog which we will use for formatting our report.
internal class PageLayout
{
public PageLayout(double height, double width)
{
this.PageHeight = height;
this.PageWidth = width;
this.Margin = new Thickness(80, 50, 80, 50);
this.ListViewItemsCount = 48;
}
public double PageHeight { get; private set; }
public double PageWidth { get; private set; }
public Thickness Margin { get; private set; }
public double PrintAreaHeight { get { return PageHeight - Margin.Top - Margin.Bottom; } }
public double PrintAreaWidth { get { return PageWidth - Margin.Left - Margin.Right; } }
public int ListViewItemsCount{ get; private set; }
}
The 'ListViewItemsCount' is most probably not the flashest coding mythology. I will elaborate in this later in this paper.
Next we want to design a Customer Report Template. We do this using XAML. The rules are no different, than if we where designing a form.
<DockPanel x:Name="LayoutDockPanel" LastChildFill="True">
<!-- Header -->
<StackPanel DockPanel.Dock="Top" Height="80">
<TextBlock FontSize="24" >Customer Report</TextBlock>
<TextBlock FontSize="24" >Full Listing</TextBlock>
</StackPanel>
<!-- Report Footer -->
<StackPanel DockPanel.Dock="Bottom" Height="20" >
<TextBlock HorizontalAlignment="Center" Margin="0,5,0,0" >Printing via a Flow Document</TextBlock>
</StackPanel>
<!-- Report Body -->
<ListView Name="ListView" HorizontalAlignment="Center" BorderThickness="0" >
<ListView.View>
<GridView>
<GridViewColumn Header="FirstName" DisplayMemberBinding="{Binding FirstName}" Width="110" />
<GridViewColumn Header="LastName" DisplayMemberBinding="{Binding LastName}" Width="110" />
<GridViewColumn Header="Phone" DisplayMemberBinding="{Binding Phone}" Width="110" />
</GridView>
</ListView.View>
</ListView>
</DockPanel>
The Print Layout I have placed in a user control and it is here where I have total control over my report content. There is a page header, page body and a page footer section. If you think this through your layout options a virtually unlimited?
Now let put this altogether! I create a Print Preview window. The XAML looks like this.
<DockPanel Width="Auto" Height="Auto" LastChildFill="True">
<Button Width="100" Height="50" DockPanel.Dock="Top" Click="SendReportToPrinterButtonClick">Send to Printer</Button>
<FlowDocumentReader >
<FlowDocument x:Name="customerReport" Background="White" />
</FlowDocumentReader>
</DockPanel>
The Code behind looks like this.
public partial class PrintPreview : Window
{
private Customers _vm;
private PrintDialog _printDialog;
public PrintPreview()
{
InitializeComponent();
_vm = new Customers();
_printDialog = new PrintDialog();
if (_printDialog.ShowDialog() == true)
{
PageLayout pageLayout = new PageLayout(_printDialog.PrintableAreaHeight, _printDialog.PrintableAreaWidth);
this.BuildReport(pageLayout);
}
}
private void BuildReport(PageLayout pageLayout)
{
// Set the page Size and Margins
this.customerReport.PagePadding = pageLayout.Margin;
this.customerReport.PageHeight = pageLayout.PageHeight;
this.customerReport.PageWidth = pageLayout.PageWidth;
this.customerReport.ColumnWidth = pageLayout.PageWidth;
this.customerReport.ColumnGap = 0;
// Body Content
for (int i = 0; i < _vm.CustomerCount; i += pageLayout.ListViewItemsCount)
{
CustomerReportTemplate layout = new CustomerReportTemplate();
// The next two lines are important as it will place the
// header and footer in place and the rest of the page area
// is for the page body content.
layout.LayoutDockPanel.Height = pageLayout.PrintAreaHeight;
layout.LayoutDockPanel.Width = pageLayout.PrintAreaWidth;
layout.ListView.ItemsSource = _vm.GetReportLineItems(i, pageLayout.ListViewItemsCount);
InlineUIContainer inlineContainer = new InlineUIContainer();
inlineContainer.Child = layout;
this.customerReport.Blocks.Add(new Paragraph(inlineContainer));
}
}
This is the code that drives the report content. Although you may see some evidence of MVVM in this snippet, it is not an objective in this paper to provide a true MVVM example.
The above snippet achieves:
- Connecting to the data source.
- Showing the Print Dialog, so that the user can select the desired printer.
- Using the information from the print dialog set up report layout.
- Calls on the data source for the report body content data.
- After the operator has studied the print preview, send the report to the printer.
I want to bring your attention to how I defined the page layout (page size, margins) and also how I have completely avoided scripting a Document Paginator. Neil Knobbe has a blog where he deployed this this mythology, which I have borrowed. He emphasises how it is important to configure the flowdocument page, column and margin sizes.
List View Item Count
In my example I assigned a value to the ListViewItemCount. This property returns the number of rows I wish to print in the body of the my document. Now obviously this value depends on the paper orientation and the paper size in your printer. So really it needs to be calculated based on these other variables. The good news is that it can be calculated. At least it can be calculated if you are happy to agree with the following logic.
- This is a business report that accompanies your application. Once this report is designed it will not change much. We may want to give the operator the choice of Portrait or Landscape. The font and font size will be pretty much static. The paper size could possibly change, but in the majority of cases you will have designed your report to fit one paper size.
- If you study my Customer Report Template above you will see I have started with a DocPanel. Added a Header and a Footer and the rest of the space is for the Page Body. The height of the body area is dependent on the Print Dialog Printable area height, our margin and the space taken up by the header and footer.
- This suggests that the item count value could be as simple as providing a portrait orientation value or a Landscape orientation value. What are these values? If you go now and change the value for ListViewItemCount to a large number (say 130) and run the reports in both portrait and landscape the the answer will be quickly evident. Give it a go.
- If you do have several paper sizes that your operator has to choose from then it's a bit more complicated. However as I leaded to above, we can easily calculate the page body height and we can also easily calculate the height of each row. The rest I will leave up to you.
This example is a good base for printing Business Reports. If I was printing many reports on my project then a lot of the above code is reusable, so it would not take a long time to push out other reports as they are required.
Source Code
You can download the Source here.