WPF Printing: How to print a PageRange with WPF’s PrintDialog – that means the user can select specific pages and only these pages are printed

Printing a Page Range isn’t as easy as it supposed to be. So in this blog-post you’ll see a very easy method to print specific pages from a XPS-Document. But before we look at the solution, let’s start with the problem.

The Problem

WPF’s PrintDialog has a UserPageRangeEnabled-Property. Set this property to true before you show the PrintDialog. Then the user is allowed to enter a Pagerange in the PrintDialog. The problem is that whatever the user enters, in each case all pages are printed. The PrintDialog itself has no logic to print some specific pages. Let’s look at an example.

Below the small UI that consists of a Button and a DocumentViewer. This code belongs to the MainWindow.xaml-File.

<Button Content="Print" Click="PrintButtonClick"
        Margin="10"
        HorizontalAlignment="Left"
        Width="75"/>
<DocumentViewer x:Name="viewer" Grid.Row="1"/>

The Codebehind-File MainWindow.xaml.cs has an EventHandler for the Loaded-Event of the Window. There a XPS-Document with five pages is loaded into memory. The FixedDocument-Instance contained in that XPS-Document is stored in the _fixedDocument-Field and displayed in the DocumentViewer called viewer – the latter we declared above in XAML.

public partial class MainWindow : Window
{
 private FixedDocument _fixedDocument;

 public MainWindow()
 {
   InitializeComponent();
 }

 private void OnLoaded(object sender, RoutedEventArgs e)
 {
    // Load the FixedDocument
  var document = new XpsDocument("FivePagesDocument.xps",
                                   FileAccess.Read);
  var sequence = document.GetFixedDocumentSequence();
  _fixedDocument = sequence.References[0].GetDocument(false);

    // Assign it to the viewer
  viewer.Document = _fixedDocument.DocumentPaginator.Source;
 }

So we have the necessary thing for testing a “print-range”-scenario: A FixedDocument-Instance in memory, stored in the _fixedDocument-Field. Let’s look at the PrintButtonClick-Eventhandler that contains the logic to print. The Eventhandler is shown below. First a PrintDialog is created. Then we set the UserPageRangeEnabled-Property to true, show the Dialog by calling the ShowDialog-Method. Is the return-value true we call the PrintDialog’s PrintDocument-Method by passing in the DocumentPaginator of the FixedDocument. Also a string is passed in that is displayed in the printer queue as jobname.

void PrintButtonClick(object sender, RoutedEventArgs e)
{
  var dlg = new PrintDialog();

  // Allow the user to select a PageRange
  dlg.UserPageRangeEnabled = true;

  if (dlg.ShowDialog() == true)
  {
    DocumentPaginator paginator = 
      _fixedDocument.DocumentPaginator;
    dlg.PrintDocument(paginator, "Just a test");
  }
}

Now what happens? Let’s look how we use the application. When starting up the application, the FixedDocument with five pages is displayed in the DocumentViewer:

image

When the “Print”-Button is pressed, the PrintDialog comes up. I select a PageRange of 2-3 like shown below. For testing purposes I always print in the PDFCreator that creates a PDF-Document with the output. There are several other ways. You could also print in the XPS-Document-Writer (even if it’s a little bit doubled when printing a xps to the XPS-Document-Writer).

image

Now even with the Pages of 2-3 selected, the “printed” PDF looks like below. It contains all five pages of my document. But it should contain only the pages 2 and 3.

image

So let’s summarize the problem: It doesn’t matter what you select as a PageRange in the PrintDialogs Pages-Field. It’s up to you to check the Pages and print out only that range. The PrintDialog doesn’t care anything about the PageRange.

The PrintDialog has the Property PrintRange that contains a PrintRange–object with PrintFrom- and PrintTo-Properties. It also has a Property called PageRangeSelection that contains the value PageRangeSelection.UserPages when a PageRange was entered. But the logic to print out that specified range is your work. And this work needs some WPF know-how to do. So let’s look at the solution(s). :-)

A bad solution

I call the first one a bad solution because it is not an easy to manage one. This solution is to make a new FixedDocument that only contains the pages you need. You can implement such a logic based on the things you find in this codeproject-article:

http://www.codeproject.com/KB/vb/reording_xps.aspx

In my mind creating a new XPS-Document just for printing is a bad solution. I think a good solution would be to just be able to print some pages of an existing xps-Document/FixedDocument. So let’s look at the good one.

A good solution (I hope so ;-))

The good solution is the one I’ve implemented today and finished in the train from Zurich to Basel. I’ve not tested every scenario, but it seems to work great. If you have any hints to this solution, let me know.

The first thing you have to know is that the pages used for the output are created by a DocumentPaginator. This was passed to the PrintDocument-Method of the PrintDialog some lines above. The DocumentPaginator-class itself is abstract. Beside some members it defines a Method GetPage(int pageNumber) that returns the DocumentPage for the passed in pageNumber. Now the idea is to create a wrapper-class for the DocumentPaginator that is itself also of type DocumentPaginator. And now guys, what pattern is that? ;-) Doesn’t matter.

So we create a DocumentPaginator and let’s call it PageRangeDocumentPaginator. It encapsulates a DocumentPaginator and can return a specific (page)range of the encapsulated DocumentPaginator. Therefore the constructor takes two parameters: a DocumentPaginator to encapsulate and a PageRange-object that contains PageFrom and PageTo-Properties, both of type int. The PageRange-structure exists in the Namespace System.Windows.Controls.

The PageRangeDocumentPaginator-class is listed below. In the constructor the passed in DocumentPaginator is stored in the _paginator-field. The pageRange-values are stored in the fields _startIndex and _endIndex. As the PageRange starts with 1, we substract 1 to get the zero-based index. In the final line of the constructor the _endIndex is adjusted. If the user enters a higher pageNumber than the Document contains, _endIndex will point to the index of the last page.

Now look at the override of the GetPage-Method. There gets just the GetPage-Method of the encapsulated DocumentPaginator called. But the _startIndex is regarded. Now look at the PageCount-Property. It uses the _startIndex and _endIndex to calculate the pageNumbers. IsPageCountValid always returns true and the Properties PageSize and Source simply pass the responsibility to the underlying DocumentPaginator. That’s it. Now look how to use the class in the PrintButtonClick-Eventhandler.

/// 
/// Encapsulates a DocumentPaginator and allows
/// to paginate just some specific pages (a "PageRange")
/// of the encapsulated DocumentPaginator
///  (c) Thomas Claudius Huber 2010 
///      http://www.thomasclaudiushuber.com
/// 
public class PageRangeDocumentPaginator : DocumentPaginator
{
  private int _startIndex;
  private int _endIndex;
  private DocumentPaginator _paginator;
  public PageRangeDocumentPaginator(
    DocumentPaginator paginator,
    PageRange pageRange)
  {
    _startIndex = pageRange.PageFrom - 1;
    _endIndex = pageRange.PageTo - 1;
    _paginator = paginator;

    // Adjust the _endIndex
    _endIndex = Math.Min(_endIndex, _paginator.PageCount - 1);
  }
  public override DocumentPage GetPage(int pageNumber)
  {
    // Just return the page from the original
    // paginator by using the "startIndex"
    return _paginator.GetPage(pageNumber + _startIndex);
  }

  public override bool IsPageCountValid
  {
    get { return true; }
  }

  public override int PageCount
  {
    get
    {
      if (_startIndex > _paginator.PageCount - 1)
        return 0;
      if (_startIndex > _endIndex)
        return 0;

      return _endIndex - _startIndex + 1;
    }
  }

  public override Size PageSize
  {
    get{return _paginator.PageSize;}
    set{_paginator.PageSize = value;}
  }

  public override IDocumentPaginatorSource Source
  {
    get { return _paginator.Source; }
  }
}

The PrintButtonClick-Eventhandler just needs a little if-statement. The PrintDialog has a Property called PageRangeSelection of Type PageRangeSelection (Enum). The PageRangeSelection-Enum contains the values AllPages and UserPages. When the User has entered a PageRange, that Property would have the value UserPages. In that case I use the PageRangeDocumentPaginator-class and pass in the DocumentPaginator of the FixedDocument and the PageRange from the PrintDialog. If the PageRangeSelection-Property contains not the value UserPages, we print all pages as we’ve already done in the problem-scenario. So the code looks like this:

void PrintButtonClick(object sender, RoutedEventArgs e)
{
  var dlg = new PrintDialog();

  // Allow the user to select a PageRange
  dlg.UserPageRangeEnabled = true;

  if (dlg.ShowDialog() == true)
  {
    DocumentPaginator paginator = 
      _fixedDocument.DocumentPaginator;

   if (dlg.PageRangeSelection == PageRangeSelection.UserPages)
   {
     paginator = new PageRangeDocumentPaginator(
                      _fixedDocument.DocumentPaginator,
                      dlg.PageRange);
   }

    dlg.PrintDocument(paginator, "Yes, it works");
  }
}

When the user has selected a PageRange the PageRangeDocumentPaginator is used to generate the pages for the output. When I test the code with a PageRange of 2-3 as in the problem-section, I now retrieve the expected output of the two pages 2 and 3, as my “printed” PDF shows:

image

That’s it. Enjoy the code and download the solution here. And don’t forget to kick this post for me:

[Download Thomas PrintingProject]

kick it on DotNetKicks.com

Cheers Thomas

PS: As time and my sparetime is limited – I’m writing on my Silverlight 4.0 book and an update of my WPF book in my sparetime – I can’t give any free support for the code. Use it or not. But feel free to contact me for any suggestions via:

http://www.thomasclaudiushuber.com/contact.php

Share this post

Comments (6)

  • SaNNy Reply

    I can`t this method on Windows XP, because the PrintDocument method throws exception:

    System.Windows.Xps.XpsSerializationException was unhandled
    Message=”FixedPage cannot contain another FixedPage.”
    Source=”ReachFramework”
    StackTrace:
    at System.Windows.Xps.Serialization.XpsSerializationManager.RegisterPageStart()
    at System.Windows.Xps.Serialization.FixedPageSerializer.PersistObjectData(SerializableObjectContext serializableObjectContext)
    at System.Windows.Xps.Serialization.ReachSerializer.SerializeObject(Object serializedObject)
    at System.Windows.Xps.Serialization.FixedPageSerializer.SerializeObject(Object serializedObject)
    at System.Windows.Xps.Serialization.DocumentPageSerializer.SerializeChild(Visual child, SerializableObjectContext parentContext)
    at System.Windows.Xps.Serialization.DocumentPageSerializer.PersistObjectData(SerializableObjectContext serializableObjectContext)
    at System.Windows.Xps.Serialization.ReachSerializer.SerializeObject(Object serializedObject)
    at System.Windows.Xps.Serialization.DocumentPageSerializer.SerializeObject(Object serializedObject)
    at System.Windows.Xps.Serialization.DocumentPaginatorSerializer.PersistObjectData(SerializableObjectContext serializableObjectContext)
    at System.Windows.Xps.Serialization.DocumentPaginatorSerializer.SerializeObject(Object serializedObject)
    at System.Windows.Xps.Serialization.XpsSerializationManager.SaveAsXaml(Object serializedObject)
    at System.Windows.Xps.XpsDocumentWriter.SaveAsXaml(Object serializedObject, Boolean isSync)
    at System.Windows.Xps.XpsDocumentWriter.Write(DocumentPaginator documentPaginator)
    at System.Windows.Controls.PrintDialog.PrintDocument(DocumentPaginator documentPaginator, String description)
    at ThomasClaudiusHuber.PrintRangeExample.MainWindow.PrintButtonClick(Object sender, RoutedEventArgs e) in D:\work\temp\printrange\MainWindow.xaml.cs:line 62
    at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)
    at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
    at System.Windows.UIElement.RaiseEventImpl(DependencyObject sender, RoutedEventArgs args)
    at System.Windows.UIElement.RaiseEvent(RoutedEventArgs e)
    at System.Windows.Controls.Primitives.ButtonBase.OnClick()
    at System.Windows.Controls.Button.OnClick()
    at System.Windows.Controls.Primitives.ButtonBase.OnMouseLeftButtonUp(MouseButtonEventArgs e)
    at System.Windows.UIElement.OnMouseLeftButtonUpThunk(Object sender, MouseButtonEventArgs e)
    at System.Windows.Input.MouseButtonEventArgs.InvokeEventHandler(Delegate genericHandler, Object genericTarget)
    at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
    at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)
    at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
    at System.Windows.UIElement.ReRaiseEventAs(DependencyObject sender, RoutedEventArgs args, RoutedEvent newEvent)
    at System.Windows.UIElement.CrackMouseButtonEventAndReRaiseEvent(DependencyObject sender, MouseButtonEventArgs e)
    at System.Windows.UIElement.OnMouseUpThunk(Object sender, MouseButtonEventArgs e)
    at System.Windows.Input.MouseButtonEventArgs.InvokeEventHandler(Delegate genericHandler, Object genericTarget)
    at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
    at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)
    at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
    at System.Windows.UIElement.RaiseEventImpl(DependencyObject sender, RoutedEventArgs args)
    at System.Windows.UIElement.RaiseEvent(RoutedEventArgs args, Boolean trusted)
    at System.Windows.Input.InputManager.ProcessStagingArea()
    at System.Windows.Input.InputManager.ProcessInput(InputEventArgs input)
    at System.Windows.Input.InputProviderSite.ReportInput(InputReport inputReport)
    at System.Windows.Interop.HwndMouseInputProvider.ReportInput(IntPtr hwnd, InputMode mode, Int32 timestamp, RawMouseActions actions, Int32 x, Int32 y, Int32 wheel)
    at System.Windows.Interop.HwndMouseInputProvider.FilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    at System.Windows.Interop.HwndSource.InputFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
    at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
    at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Boolean isSingleParameter)
    at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)
    at System.Windows.Threading.Dispatcher.WrappedInvoke(Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)
    at System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Boolean isSingleParameter)
    at System.Windows.Threading.Dispatcher.Invoke(DispatcherPriority priority, Delegate method, Object arg)
    at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
    at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
    at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
    at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
    at System.Windows.Threading.Dispatcher.Run()
    at System.Windows.Application.RunDispatcher(Object ignore)
    at System.Windows.Application.RunInternal(Window window)
    at System.Windows.Application.Run(Window window)
    at System.Windows.Application.Run()
    at ThomasClaudiusHuber.PrintRangeExample.App.Main() in D:\work\temp\printrange\obj\Debug\App.g.cs:line 0
    at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
    at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
    at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
    at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
    at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
    at System.Threading.ThreadHelper.ThreadStart()
    InnerException:

    December 12, 2009 at 9:29 pm
  • SaNNy Reply

    The exception throwns where i using XPS Printer on Windows XP or Windows 7.
    But for PDF printer no throwns was detected, it`s ok

    December 13, 2009 at 10:13 am
  • Sidnei Reply

    Same error with all of my printers.
    P.s: PDF print not tested.

    January 31, 2010 at 4:58 pm
  • mikroba Reply

    I have same error “FixedPage cannot contain another FixedPage.”

    July 16, 2010 at 11:33 am
  • DanyW Reply

    Thanks for the code – we are currently using this and works just fine with XpsDocumentWriter.Write method. However, it fails when we use the WriteAsync method. Have you tried Async printing with this?

    January 9, 2011 at 10:22 pm
  • Hosein Reply

    For the mentioned problem in the comments I suggested a solution on http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/841e804b-9130-4476-8709-0d2854c11582/.

    If you’ve found a problem with this solution please report it over there.

    By the way thank you Thomas for your great and simple solution for this PageRange problem.

    Hosein.

    December 24, 2011 at 3:25 pm

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.