Chapter 7. Working with Text, Documents, and Printing

Document handling and printing have been greatly overhauled in .NET 3.5. With the introduction of XPS documents, creating, editing, managing, and printing documents have all been hugely simplified. It is now possible to quickly provide print functionality for various object types and user scenarios.

The new printing and document functionality is built on the XML Paper Specification (XPS) technology. XPS defines an electronic file format, a spool file format, and a page description format on which the new XPS print path is built. The print path allows a document to remain in an XPS format from creation to the final processing of a printing device.

With the added richness of WPF controls and handling of text, programmatically manipulating the way text is displayed and used has been made easier than traditional approaches.

The recipes in this chapter describe how to:

Programmatically Insert Text into a RichTextBox

Problem

You need to programmatically insert text into a System.Windows.Controls.RichTextBox, with the text being inserted at the caret's position.

Solution

Get a System.Windows.Documents.TextPointer representing the caret's current position in the RichTextBox, and then use the InsertTextIntoRun method on the TextPointer, passing in the text to insert as a System.String.

How It Works

The TextPointer object is provided to help move through text elements in a flow content, providing methods to examine, manipulate, or add text elements into the flow content with which the TextPointer is associated.

A RichTextBox control tracks the position of the caret and stores this in a TextPointer, accessible through the CaretPosition property. This allows easy access to the caret's position, enabling you to easily insert any text at the caret's position.

The TextPointer object provides an InsertTextIntoRun method, which takes a single argument, a String, containing the text to insert. The method inserts the supplied text into the current System.Windows.Document.Run that the TextPointer is in. If the TextPointer is not in the scope of a Run, a new Run is created and inserted into the flow content.

If the user has selected some text in the RichTextBox control that should be replaced by the text to be inserted, the selected text must be cleared first. This is easily achieved by modifying the RichTextBox control's Selection property. This property stores a System.Windows.Documents.TextSelection object (which derives from System.Windows.Documents.TextRange). The Text property of the TextSelection can be set to String.Empty, thus clearing the text.

After the new text is inserted, the caret is positioned at the start of the inserted text by default. To ensure that the caret is moved to the end of the newly inserted text, you'll need to obtain a reference to a TextPointer that points to the next element after the caret. Once the text has been inserted, the caret's position can be set to the recently obtained TextPointer, thus positioning the caret at the end of the inserted text. Getting the next element on from the caret is achieved using the GetPositionAtOffset method on a TextPointer.

The GetPositionAtOffset method takes two arguments and returns a TextPointer. The first is a System.Int32 indicating the offset, in symbols, from the beginning of the TextPointer. The second argument is a System.Windows.LogicalDirection and specifies the direction in which to move.

By calling the BeginChange method on the RichTextBox control before inserting the text, you can ensure that no text content or selection changed events will be raised until the EndChange method is called. This means the insertion can take place more efficiently because it will not be interrupted by code possibly listening to and acting on such events, such as syntax highlighting and so on. Another advantage is that any changes made after BeginChange and before EndChange will be combined into a single undo action.

The Code

The following example gives the XAML and code-behind used to display a window containing a RichTextBox with a System.Windows.Controls.TextBox and a System.Windows.Controls.Button. Both the RichTextBox and TextBox can be edited. The TextBox at the bottom of the window is where the insertion text is entered. Clicking the Button will take the text from the TextBox and insert it into the RichTextBox at the location of the caret.

<Window
  x:Class="Recipe_07_01.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1" Height="600" Width="800">
  <DockPanel>
    <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
      <TextBox x:Name="tbxInsertionText" Width="200" Margin="5,0" />
      <Button
        DockPanel.Dock="Bottom"
        Content="Insert"
        Width="40"
        Margin="5,0"
        Click="btnInsert_Click"
      />
    </StackPanel>
    <RichTextBox x:Name="rtbTextContent" />
  </DockPanel>
</Window>

The following code describes the content of the previous markup's code-behind file:

using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;

namespace Recipe_07_01
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }
private void btnInsert_Click(object sender, RoutedEventArgs e)
        {
            // Check to see we have some valid insertion text.
            if (string.IsNullOrEmpty(tbxInsertionText.Text))
            {
                return;
            }

            // Mark the text control as being changed. This prevents
            // any text content or selection changed events and
            // combines any steps performed before the EndChange into
            // a single undo action.
            rtbTextContent.BeginChange();

            // First clear any selected text.
            if (rtbTextContent.Selection.Text != string.Empty)
            {
                rtbTextContent.Selection.Text = string.Empty;
            }

            // Get the text element adjacent to the caret in its current
            // position.
            TextPointer tp =
                rtbTextContent.CaretPosition.GetPositionAtOffset(0,
                    LogicalDirection.Forward);

            // Insert the text we have supplied
            rtbTextContent.CaretPosition.InsertTextInRun(tbxInsertionText.Text);

            // Now restore the caret's position so that it is placed
            // after the newly inserted text.
            rtbTextContent.CaretPosition = tp;

            //We have finished making our changes.
            rtbTextContent.EndChange();

            // Now set the focus back to RichTextBox so the user can
            // continue typing.
            Keyboard.Focus(rtbTextContent);
        }
    }
}

Apply Syntax Highlighting in a Text Control

Problem

You need to color words in a System.Windows.Controls.RichTextBox based on some rule set; for example, all numerical characters are given a bold font weight.

Solution

Use a System.Windows.Documents.TextPointer to work your way through each element in the RichTextBox's document, and apply the appropriate formatting.

How It Works

When the content of the RichTextBox is changed, the content of the RichTextBox is formatted according to some simple rules.

Before the formatting is applied, any existing formatting first needs to be cleared. This is done by creating a System.Windows.Documents.TextRange, which spans the entire content of the RichTextBox, and then calling the ClearAllProperties method on the TextRange.

To apply some simple formatting in this example, each block of text in the document is examined to see whether it is one of four operators (+, -, /, *), an integer, or text. Based on this, the block of text is formatted by modifying the value of a property on the TextRange. This property can be any dependency property defined in the System.Windows.Documents.TextElement class.

The method that finds the next block of text works by looking at the category of the content adjacent to the TextPointer. The category is defined using the System.Windows.Documents.TextPointerContext enum. If this value is anything other than TextPointerContext.Text, the method continues to examine the next content element.

The Code

<Window
  x:Class="Recipe_07_02.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"
  Height="600"
  Width="800">
  <Grid>
    <RichTextBox
      x:Name="rtbTextContent"
      TextChanged="RichTextBox_TextChanged"
    />
  </Grid>
</Window>

The following code defines the content of the previous markup's code-behind file:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

namespace Recipe_07_02
{
    public enum TokenType
    {
        Numerical,
        Operator,
        Other
    }

    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        private void RichTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            // Get the content from the rich text box.
            TextRange textRange =
                new TextRange(rtbTextContent.Document.ContentStart,
                              rtbTextContent.Document.ContentEnd);

            // We don't want to know about any more changes while we're
            // making changes.
            rtbTextContent.TextChanged -= RichTextBox_TextChanged;

            // First clear any formatting applied to the text.
            textRange.ClearAllProperties();

            ApplyFormatting();

            // Start listening for changes again.
            rtbTextContent.TextChanged += RichTextBox_TextChanged;
        }
private void ApplyFormatting()
        {
            // We want to start from the beginning of the document.
            TextPointer tp = rtbTextContent.Document.ContentStart;

            //Find the next block of text.
            tp = FindNextString(tp);

            while (tp != null)
            {
                TextPointer textRangeEnd = tp.GetPositionAtOffset(1,
                    LogicalDirection.Forward);

                TextRange tokenTextRange = new TextRange(tp,
                    tp.GetPositionAtOffset(1, LogicalDirection.Forward));

                TokenType tokenType = ClassifyToken(tokenTextRange.Text);

                switch (tokenType)
                {
                    case TokenType.Numerical:
                        tokenTextRange.ApplyPropertyValue(
                            TextElement.ForegroundProperty, Brushes.Blue);
                        break;
                    case TokenType.Operator:
                        tokenTextRange.ApplyPropertyValue(
                            TextElement.FontWeightProperty, FontWeights.Bold);
                        break;
                    case TokenType.Other:
                        tokenTextRange.ApplyPropertyValue(
                            TextElement.FontSizeProperty, 20d);
                        break;
                }

                tp = FindNextString(textRangeEnd);
            }
        }

        private TokenType ClassifyToken(string text)
        {
            int temp;

            if (int.TryParse(text, out temp))
            {
                return TokenType.Numerical;
            }
switch(text)
                {
                    case "+":
                    case "-":
                    case "/":
                    case "*":
                        return TokenType.Operator;
                    default:
                        return TokenType.Other;
                }
        }

        private TextPointer FindNextString(TextPointer tp)
        {
            //Skip over anything that isn't text
            while (tp.GetPointerContext(
                LogicalDirection.Forward) != TextPointerContext.Text)
            {
                tp = tp.GetPositionAtOffset(1, LogicalDirection.Forward);

                if (tp == null)
                {
                    return tp;
                }
            }

            //Skip over any whitespace we meet
            char[] buffer = new char[1];
            tp.GetTextInRun(LogicalDirection.Forward, buffer, 0, 1);

            while (IsWhiteSpace(buffer))
            {
                tp = tp.GetPositionAtOffset(1, LogicalDirection.Forward);

                if (tp == null)
                {
                    break;
                }

                tp.GetTextInRun(LogicalDirection.Forward, buffer, 0, 1);
            }

            return tp;
        }
private bool IsWhiteSpace(char[] buffer)
        {
            return (buffer[0] == '
'
                    || buffer[0] == '	'
                    || buffer[0] == '
'
                    || buffer[0] == ' '),
        }
    }
}

Print a WPF Visual

Problem

You need to print a visual element within your application.

The Solution

Use a System.Windows.Xps.XpsDocumentWriter to print a visual object to a System.Printing.PrintQueue.

How It Works

The Write method of a System.Windows.Documents.XpsDocumentWriter has several overloads that allow several different object types to be written to the target against which the XpsDocumentWriter was created. One of these overloads takes a single argument of type System.Windows.Media.Visual. By creating the XpsDocumentWriter against a PrintQueue, content written by the XpsDocumentWriter will be written to the PrintQueue.

You can create a PrintQueue using several different approaches. These are two examples of creating a PrintQueue:

  • Selecting a PrintQueue from a selection of printers discovered using a System.Printing.PrintServer object

  • Presenting the user with a System.Windows.Dialogs.PrintDialog where a target printer is selected

The method of choice will depend on the level of user interaction required and the security levels of the machine. Of the previous two examples for obtaining a PrintQueue, a PrintServer object can be instantiated only in a full-trust environment, whereas the PrintDialog approach will work in both full and partial-trust environments.

To ensure that the visual being printed appears within the bounds of the printed page, it may be necessary to scale the visual before it is written to the PrintQueue. Without scaling the visual, it will be printed at the size it appears on the screen. If this size is greater than that of the paper size (taking into account the resolution of the printout), the visual will appear clipped. The target page size can be obtained through the PrintQueue's System.Printing.PrintTicket. (See recipe 7-5 for more information on the PrintTicket class.)

Note

You will need to add references to the System.Printing and ReachFramework assemblies in your project for this example.

The Code

This example shows you how to print a Visual object using a PrintQueue obtained from a PrintDialog. The following XAML defines a window with a few visual objects that are to be printed. A button at the bottom of the form calls a method in the code-behind that initiates the printing process.

<Window
  x:Class="Recipe_07_03.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1">
  <Grid>
    <Grid x:Name="VisualRoot">
      <Ellipse
        Fill="Blue"
        Height="300"
        Width="300"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
      />

      <TextBlock
        FontSize="24"
        Foreground="White"
        Text="A Printed Visual"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
      />
    </Grid>

    <Button
      Click="btnPrintVisual_Click"
      Content="Print Visual..."
      Width="150"
      VerticalAlignment="Bottom"
      HorizontalAlignment="Center"
    />
  </Grid>
</Window>

The following code-behind contains a handler for the click event of the button defined earlier. When the button is clicked, a PrintDialog is presented to the user, from which they select the printer to which they want to print.

If the dialog box is closed via the OK button, a PrintQueue for the selected printer is obtained, and an XpsDocumentWriter is created. The visual is then written to the PrintQueue using the Write method on the XpsDocumentWriter.

using System.Printing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Xps;

namespace Recipe_07_03
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        private Visual GetVisual()
        {
            return new Grid();
        }

        private void btnPrintVisual_Click(object sender, RoutedEventArgs e)
        {
            //Get hold of the visual you want to print.
            Visual visual = GetVisual();

            // Create a Print dialog.
            PrintDialog printDialog = new PrintDialog();

            if (printDialog.ShowDialog() != true)
            {
                return;
            }

            // Get the default print queue
            PrintQueue pq = printDialog.PrintQueue;

            //Scale the visual
            Visual scaledVisual = ScaleVisual(visual, pq);
// Get an XpsDocumentWriter for the default print queue
            XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(pq);

            xpsdw.Write(scaledVisual);
        }

        //We want to be able to scale the visual so it fits within the page.
        private Visual ScaleVisual(Visual v, PrintQueue pq)
        {
            ContainerVisual root = new ContainerVisual();
            const double inch = 96;

            // Set the margins.
            double xMargin = 1.25 * inch;
            double yMargin = 1 * inch;

            PrintTicket pt = pq.UserPrintTicket;
            double printableWidth = pt.PageMediaSize.Width.Value;
            double printableHeight = pt.PageMediaSize.Height.Value;

            double xScale = (printableWidth - xMargin * 2) / printableWidth;
            double yScale = (printableHeight - yMargin * 2) / printableHeight;

            root.Children.Add(v);
            root.Transform
                = new MatrixTransform(xScale, 0, 0, yScale, xMargin, yMargin);

            return root;
        }
    }
}

Print a Collection of WPF Visuals

Problem

You have a collection of System.Windows.Media.Visual objects that you want to print, with each Visual being printed on a separate page.

Solution

Write the Visuals to a System.Printing.PrintQueue using the batched write capability of a System.Windows.Xps.VisualsToXpsDocument object.

How It Works

In recipe 7-3, a single Visual is printed by obtaining a reference to an XpsDocumentWriter and calling its Write method, passing in the Visual to be printed. To print a collection of visual objects, you must use a System.Windows.Xps.VisualsToXpsDocument object (an implementation of a System.Windows.Documents.Serialization.SerializerWriterCollator) to perform a batch write of the visuals to the PrintQueue.

To obtain a VisualsToXpsDocument, you first create a System.Windows.Xps.XpsDocumentWriter against a PrintQueue, as described in recipe 7-3. Using the CreateVisualCollator method on the XpsDocumentWriter, you get an instance of a VisualsToXpsWriterDocument object.

To write the Visual objects to the PrintQueue, the Write method on the VisualsToXpsDocument is called for each Visual object you want to print, creating a new System.Windows.Documents.FixedPage for each Visual in the document. As such, only visual elements that can be written to a FixedPage can be passed to the Write method of the VisualsToXpsDocument.

Once each Visual has been written, a call to EndBatchWrite on the VisualsToXpsDocument is called, writing the document to the PrintQueue.

Note

You will need to add references to the System.Printing and ReachFramework assemblies in your project for this example.

The Code

<Window
  x:Class="Recipe_07_04.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1">
  <Grid>
    <Grid x:Name="VisualRoot">
      <Ellipse
        Fill="Blue"
        Height="300"
        Width="300"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
      />

      <TextBlock
        FontSize="24"
        Foreground="White"
        Text="A Printed Visual"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
      />
    </Grid>
<Button
      Click="btnPrintVisuals_Click"
      Content="Print Visuals..."
      Width="150"
      VerticalAlignment="Bottom"
      HorizontalAlignment="Center"
    />
  </Grid>
</Window>

The following code-behind defines a handler for the click event of the System.Windows.Controls.Button defined earlier. A System.Windows.Dialogs.PrintDialog is created and presented to the user. If the PrintDialog is closed by clicking the OK button, a collection of Visual objects is sent to the chosen printer using a VisualsToXpsDocument collator.

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

namespace Recipe_07_04
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        private List<Visual> GetVisuals()
        {
            return new List<Visual>(new Visual[]
                                        {
                                            VisualRoot,
                                            VisualRoot,
                                            VisualRoot
                                        });
        }

        private void btnPrintVisuals_Click(object sender, RoutedEventArgs e)
        {
            //Get hold of the visual you want to print.
            List<Visual> visuals = GetVisuals();
// Create a Print dialog.
            PrintDialog printDialog = new PrintDialog();

            if (printDialog.ShowDialog() != true)
            {
                return;
            }

            // Get the default print queue
            PrintQueue printQueue = printDialog.PrintQueue;

            // Get an XpsDocumentWriter for the default print queue
            XpsDocumentWriter xpsdw
                = PrintQueue.CreateXpsDocumentWriter(printQueue);

            VisualsToXpsDocument vtxd =
                (VisualsToXpsDocument)xpsdw.CreateVisualsCollator();

            //Indicate we want any writes to be performed in a batch operation.
            vtxd.BeginBatchWrite();

            //Write out each visual.
            visuals.ForEach(delegate(Visual visual)
            {
                //Scale the visual
                Visual scaledVisual = ScaleVisual(visual, printQueue);

                vtxd.Write(scaledVisual);
            });

            //Mark the end of the batch operation.
            vtxd.EndBatchWrite();
        }

        //We want to be able to scale the visual so it fits within the page.
        private Visual ScaleVisual(Visual visual, PrintQueue printQueue)
        {
            ContainerVisual root = new ContainerVisual();

            //An inch is 96 DIPs, use this to scale up sizes given in inches.
            double inch = 96;

            //Calculate our margins.
            double xMargin = 1.25 * inch;
            double yMargin = 1 * inch;
//Get the current print ticket from which the paper size can be
            //obtained.
            PrintTicket printTicket = printQueue.UserPrintTicket;

            //Retrieve the dimensions of the target page.
            double pageWidth = printTicket.PageMediaSize.Width.Value;
            double pageHeight = printTicket.PageMediaSize.Height.Value;

            double xScale = (pageWidth - xMargin * 2) / pageWidth;
            double yScale = (pageHeight - yMargin * 2) / pageHeight;

            root.Children.Add(visual);

            root.Transform
                = new MatrixTransform(xScale, 0, 0, yScale, xMargin, yMargin);

            return root;
        }
    }
}

Configure Printing Options Using a PrintTicket

Problem

You need to be able to check for and configure the available features of a printer such as duplexing, stapling, collation, page size, and so on.

Solution

Using the GetPrintCapabilities method of a System.Printing.PrintQueue, it is possible to detect what functionality is available on a given printer. By configuring the System.Printing.PrintTicket property on the PrintQueue, functionality can be enabled or disabled as required.

How It Works

The GetPrintCapabilities method on a PrintQueue returns a System.Printing.PrintCapabilities, defining what functionality the printer provides. The PrintCapabilities object represents a PrintCapabilities document, which is an XML document detailing a printer's capabilities and current settings.

For each feature that the printer offers, an element appears in the document; for example, if the printer supported collation, there would exist a Collation element. The PrintCapabilities object contains properties for each possible feature, for example, CollationCapability. The CollationCapability property is a collection of System.Printing.Collation, indicating the collation capabilities of the printer. If the printer does not support a feature, the value of the capability property is set to null.

To configure the printing options, the appropriate property on the PrintTicket object is set to the desired value.

Note

You will need to add references to the System.Printing and ReachFramework assemblies in your project for this example.

The Code

<Window
  x:Class="Recipe_07_05.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"
  Height="300"
  Width="300"
  Loaded="Window_Loaded">
  <StackPanel>
    <GroupBox
      x:Name="gbStage1"
      Header="Stage 1 - Select a Printer"
      BorderBrush="Black"
      Margin="5">
      <Button
        Content="Select Printer..."
        Margin="5"
        Click="btnSelectPrinter_Click" />
    </GroupBox>

    <GroupBox
      x:Name="gbStage2"
      Header="Stage 2 - Configure Options"
      BorderBrush="Black"
      Margin="5">
      <WrapPanel>

        <GroupBox Header="Duplexing" Margin="5,0,2.5,0">
          <StackPanel>
            <RadioButton
              x:Name="rbDuplexing1"
              GroupName="duplexing"
              Content="One Sided" />
            <RadioButton
              x:Name="rbDuplexing2"
              GroupName="duplexing"
              Content="Two Sided (Long Edge)" />
<RadioButton
              x:Name="rbDuplexing3"
              GroupName="duplexing"
              Content="Two Sided (Short Edge)" />
          </StackPanel>
        </GroupBox>

        <GroupBox Header="Collation" Margin="2.5,0">
          <StackPanel>
            <RadioButton
              x:Name="rbCollation1"
              GroupName="collation"
              Content="Collated" />
            <RadioButton
              x:Name="rbCollation2"
              GroupName="collation"
              Content="Uncollated" />
          </StackPanel>
        </GroupBox>

        <GroupBox Header="Duplexing" Margin="2.5,0,5,0">
          <StackPanel>
            <RadioButton
              x:Name="rbOrientation1"
              GroupName="orientation"
              Content="Landscape" />
            <RadioButton
              x:Name="rbOrientation2"
              GroupName="orientation"
              Content="Portrait" />
          </StackPanel>
        </GroupBox>
      </WrapPanel>
    </GroupBox>

    <GroupBox
      x:Name="gbStage3"
      Header="Stage 3 - Print"
      BorderBrush="Black"
      Margin="5">
      <Button
        Content="Print"
        Margin="5"
        Click="btnPrint_Click" />
    </GroupBox>
  </StackPanel>
</Window>

The following code defines the content of the Window1.xaml.cs file:

using System.Printing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Markup;
using System.Windows.Xps;

namespace Recipe_07_05
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        private PrintQueue printQueue;
        private PrintTicket printTicket;

        public Window1()
        {
            InitializeComponent();
        }

        //When the Window loads, set the initial control states.
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            gbStage1.IsEnabled = true;
            gbStage2.IsEnabled = false;
            gbStage3.IsEnabled = false;
        }

        private void btnSelectPrinter_Click(object sender, RoutedEventArgs e)
        {
            //Set the state of the options controls
            printQueue = GetPrintQueue();

            if (printQueue == null)
            {
                return;
            }

            // Get default PrintTicket from printer
            printTicket = printQueue.UserPrintTicket;

            PrintCapabilities printCapabilites = printQueue.GetPrintCapabilities();
SetControlStates(printCapabilites, printTicket);
            }

            private void btnPrint_Click(object sender, RoutedEventArgs e)
            {
                SetPrintTicket(printTicket);

                XpsDocumentWriter documentWriter =
                    PrintQueue.CreateXpsDocumentWriter(printQueue);

                documentWriter.Write(CreateMultiPageFixedDocument(), printTicket);

                MessageBox.Show("Document printed.",
                                "Recipe 07 05",
                                MessageBoxButton.OK,
                                MessageBoxImage.Information);
            }

            private PrintQueue GetPrintQueue()
            {
                // Create a Print dialog.
                PrintDialog printDialog = new PrintDialog();

                if (printDialog.ShowDialog() != true)
                {
                    return null;
                }

                // Get the default print queue
                PrintQueue printQueue = printDialog.PrintQueue;

                return printQueue;
            }

            public FixedDocument CreateMultiPageFixedDocument()
            {
                FixedDocument fixedDocument = new FixedDocument();

                //Set the size of each page to be A4 (8.5" x 11").
                Size a4PageSize = new Size(8.5 * 96, 11 * 96);
                fixedDocument.DocumentPaginator.PageSize = a4PageSize;

                //Add 5 pages to the document.
                for (int i = 1; i < 6; i++)
                {
                    PageContent pageContent = new PageContent();
                    fixedDocument.Pages.Add(pageContent);
FixedPage fixedPage = new FixedPage();
                    //Create a TextBlock
                    TextBlock textBlock = new TextBlock();
                    textBlock.Margin = new Thickness(10, 10, 0, 0);
                    textBlock.Text = string.Format("Page {0}", i);
                    textBlock.FontSize = 24;
                    //Add the TextBlock to the page.
                    fixedPage.Children.Add(textBlock);
                    //Add the page to the page's content.
                    ((IAddChild)pageContent).AddChild(fixedPage);
                }

                return fixedDocument;
            }

            //Set the states of the controls defined in the markup
            //for this Window.
            private void SetControlStates(
                PrintCapabilities printCapabilities,
                PrintTicket printTicket)
            {
                gbStage1.IsEnabled = false;
                gbStage2.IsEnabled = true;
                gbStage3.IsEnabled = true;

                //Set duplexing options.
                rbDuplexing1.IsEnabled =
                    printCapabilities.DuplexingCapability.Contains(
                        Duplexing.OneSided);

                rbDuplexing1.IsChecked =
                    printTicket.Duplexing == Duplexing.OneSided;

                rbDuplexing2.IsEnabled =
                    printCapabilities.DuplexingCapability.Contains(
                        Duplexing.TwoSidedLongEdge);

                rbDuplexing2.IsChecked =
                    printTicket.Duplexing == Duplexing.TwoSidedLongEdge;

                rbDuplexing3.IsEnabled =
                    printCapabilities.DuplexingCapability.Contains(
                        Duplexing.TwoSidedShortEdge);

                rbDuplexing3.IsChecked =
                    printTicket.Duplexing == Duplexing.TwoSidedShortEdge;
//Set collation properties.
                rbCollation1.IsEnabled =
                    printCapabilities.CollationCapability.Contains(
                        Collation.Collated);

                rbCollation1.IsChecked =
                    printTicket.Collation == Collation.Collated;

                rbCollation2.IsEnabled =
                    printCapabilities.CollationCapability.Contains(
                        Collation.Uncollated);

                rbCollation2.IsChecked =
                    printTicket.Collation == Collation.Uncollated;

                //Set the orientation properties
                rbOrientation1.IsEnabled =
                    printCapabilities.PageOrientationCapability.Contains(
                        PageOrientation.Landscape);

                rbOrientation1.IsChecked =
                    printTicket.PageOrientation == PageOrientation.Landscape;

                rbOrientation2.IsEnabled =
                    printCapabilities.PageOrientationCapability.Contains(
                        PageOrientation.Portrait);

                rbOrientation2.IsChecked =
                    printTicket.PageOrientation == PageOrientation.Portrait;
            }

            private void SetPrintTicket(PrintTicket printTicket)
            {
                //Determine the Duplexing value.
                if (rbDuplexing1.IsEnabled
                    && rbDuplexing2.IsChecked == true)
                {
                    printTicket.Duplexing = Duplexing.OneSided;
                }
                else if (rbDuplexing2.IsEnabled
                         && rbDuplexing2.IsChecked == true)
                {
                    printTicket.Duplexing = Duplexing.TwoSidedLongEdge;
                }
                else if (rbDuplexing3.IsEnabled
                         && rbDuplexing3.IsChecked == true)
{
               printTicket.Duplexing = Duplexing.TwoSidedShortEdge;
           }

           //Determine the Collation setting.
           if (rbCollation1.IsEnabled
               && rbDuplexing2.IsChecked == true)
           {
               printTicket.Collation = Collation.Collated;
           }
           else if (rbCollation2.IsEnabled
               && rbDuplexing2.IsChecked == true)
           {
               printTicket.Collation = Collation.Uncollated;
           }

           //Determine the Orientation value.
           if (rbOrientation1.IsEnabled
               && rbOrientation1.IsChecked == true)
           {
               printTicket.PageOrientation = PageOrientation.Landscape;
           }
           else if (rbOrientation2.IsEnabled
                    && rbOrientation2.IsChecked == true)
           {
               printTicket.PageOrientation = PageOrientation.Portrait;
           }
        }
    }
}

Print a Simple Document

Problem

You have a System.Windows.Documents.FixedDocument or System.Windows.Documents.FlowDocument containing a single page that you want to print. The document may have been created program-matically or loaded from an XPS package.

Solution

Printing a FixedDocument or FlowDocument is a relatively simple process and not too unlike that of printing a Visual (recipe 7-3). Obtain a System.Windows.Printing.PrintQueue object, and use it to create a System.Windows.Xps.XpsDocumentWriter. The XpsDocumentWriter is used to write the document using overloaded versions of the Write method, writing the document to the PrintQueue against which it was created.

Note

You will need to add references to the System.Printing and ReachFramework assemblies in your project for this example.

How It Works

In this recipe, a PrintQueue is obtained through displaying a System.Windows.Dialogs.PrintDialog to the user, allowing the user to choose a printer to print the document on (see recipe 7-3 for more information on obtaining a PrintQueue). From this, a PrintQueue object is obtained, and an XpsDocumentWriter is created using the CreateXpsDocumentWriter method on the PrintQueue.

The document that is to be printed can then be passed to the Write method of the XpsDocumentWriter. For FixedDocument objects, the FixedDocument itself is passed to the write method, because XPS documents are themselves fixed documents. For FlowDocument objects, though, you need to get the document's DocumentPaginator to pass in. This is achieved by casting the FlowDocument object to an IDocumentPaginatorSource and reading the DocumentPaginator property. This can then be passed to the XpsDocumentWriter.

Each page in the document will be printed onto a separate page of paper and will be sent to the printer at full size that it appears in the document. As such, you may want to scale each page to the size selected by the user in the PrintDialog. You can find the target paper size by looking at the System.Printing.PrintTicket that gets created based on the user's choices in the PrintDialog.

The Code

<Window
  x:Class="Recipe_07_06.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"
  Height="100"
  Width="300">
  <StackPanel>
    <Button
      Margin="10,5"
      Content="Print FixedDocument"
      Click="btnPrintFixedDocument_Click"
    />
    <Button
       Margin="10,5"
       Content="Print FlowDocument"
       Click="btnPrintFlowDocument_Click"
    />
  </StackPanel>
</Window>
using System.Printing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Xps;

namespace Recipe_07_06
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        public FixedDocument GetFixedDocument()
        {
            // Create a FixedDocument
            FixedDocument fixedDocument = new FixedDocument();
            //Set the size of each page to be A4.
            Size a4PageSize = new Size(8.5 * 96, 11 * 96);
            fixedDocument.DocumentPaginator.PageSize = a4PageSize;

            //Add 5 pages to the document.
            for (int i = 1; i < 6; i++)
            {

                PageContent pageContent = new PageContent();
                fixedDocument.Pages.Add(pageContent);

                FixedPage fixedPage = new FixedPage();
                //Create a TextBlock
                TextBlock textBlock = new TextBlock();
                textBlock.Margin = new Thickness(10, 10, 0, 0);
                textBlock.Text = string.Format("Page {0}", i);
                textBlock.FontSize = 24;
                //Add the TextBlock to the page.
                fixedPage.Children.Add(textBlock);
                //Add the page to the page's content.
                ((IAddChild)pageContent).AddChild(fixedPage);
             }
return fixedDocument;
}

public FlowDocument GetFlowDocument()
{
    //Programmatically create a FlowDocument
    FlowDocument flowDocument = new FlowDocument();

    //Create a new paragraph to add to the document.
    Paragraph paragraph = new Paragraph();

    //Add some text to the paragraph.
    paragraph.Inlines.Add("This is the printed document.");

    //Add the paragraph to the document.
    flowDocument.Blocks.Add(paragraph);

    //Create a new figure and add an Ellipse to it.
    Figure figure = new Figure();
    paragraph = new Paragraph();
    Ellipse ellipse = new Ellipse();
    ellipse.Width = 50;
    ellipse.Height = 50;
    ellipse.Fill = Brushes.Red;
    ellipse.StrokeThickness = 2;
    ellipse.Stroke = Brushes.Black;
    paragraph.Inlines.Add(ellipse);

    //Add the figure to a paragraph.
    figure.Blocks.Add(paragraph);

    //Insert the figure into a new paragraph.
    flowDocument.Blocks.Add(new Paragraph(Figure));

    //Add a final paragraph
    paragraph = new Paragraph();
    paragraph.Inlines.Add("This text is not intended to be read.");

    flowDocument.Blocks.Add(paragraph);

    return flowDocument;

}
//Obtain a reference to a PrintQueue using a PrintDialog.
public PrintQueue GetPrintQueue()
{
    PrintDialog printDialog = new PrintDialog();

    bool? result = printDialog.ShowDialog();

    if (result.HasValue && result.Value)
    {
        return printDialog.PrintQueue;
    }

    return null;
}

//Prints a FlowDocument
public void PrintFlowDocument(PrintQueue printQueue)
{
    FlowDocument flowDocument = GetFlowDocument();

    DocumentPaginator documentPaginator =
        ((IDocumentPaginatorSource)flowDocument).DocumentPaginator;

    XpsDocumentWriter writer =
        PrintQueue.CreateXpsDocumentWriter(printQueue);

    writer.Write(documentPaginator);
}

//Prints a FixedDocument
public void PrintFixedDocument(PrintQueue printQueue)
{
    FixedDocument fixedDocument = GetFixedDocument();

    XpsDocumentWriter writer =
        PrintQueue.CreateXpsDocumentWriter(printQueue);

    writer.Write(fixedDocument);
}

//Event handler for the click event of the Print FixedDocument button.
private void btnPrintFixedDocument_Click(object sender, RoutedEventArgs e)
{
    PrintFixedDocument(GetPrintQueue());
}
//Event handler for the click event of the Print FlowDocument button.
    private void btnPrintFlowDocument_Click(object sender, RoutedEventArgs e)
    {
        PrintFlowDocument(GetPrintQueue());
    }
  }
}

Asynchronously Print a Multipage FixedDocument

Problem

You need to print a multipage System.Windows.Documents.FlowDocument with each page being printed onto a separate page of paper. Because your multipage document could be quite large, you will want to perform the printing asynchronously, keeping the user informed of the progress and allowing them to cancel the print job if they want.

Solution

Use the System.Windows.Xps.XpsDocumentWriter.WriteAsync method, hooking into its various events to keep the user informed of the printing progress as well as to offer the chance to cancel the job.

Note

You will need to add references to the System.Printing and ReachFramework assemblies in your project for this example.

How It Works

When printing a document using the Write method of an XpsDocumentWriter, the calling thread will wait until the Write method has completed. This is fine for small documents, but for larger documents, the Write method will take longer, preventing any further work on the UI thread.

To maintain a responsive UI, provide the user with feedback on the progress of the printing, and allow the printing to be cancelled, the printing needs to be performed asynchronously. Luckily, the XpsDocumentWriter easily allows for asynchronous printing with only a little extra code.

Obtaining a System.Windows.Printing.PrintQueue and XpsDocumentWriter for asynchronous printing is the same as when printing a Visual (see recipe 7-3). Once an XpsDocumentWriter has been created, instead of calling Write, WriteAsync is used. The extra code required is in adding handlers to the WritingProgressChanged and WritingCompleted events, which are raised by the XpsDocumentWriter once the writing begins. Code to cancel the printing is also required.

The WritingProgressChanged event is raised continuously by the XpsDocumentWriter as it writes each document part. A System.Windows.Documents.Serialization. WritingProgressChangedEventArgs object is passed to any methods handling the event. The event arguments detail the number of pages that have been written and a WritingProgressChangeLevel value indicating the scope of the event. The WritingProgressChangeLevel will be one of four possible values:

  • FixedDocumentSequenceWritingProgress

  • FixedDocumentWritingProgress

  • FixedPageWritingProgress

  • None

In this example, the value will be either FixedDocumentWritingProgress as pages are written or FixedDocumentSequenceWritingProgress as the final part of the document is written, indicating the printing is almost complete. Using the information in the event arguments, the user can be updated on the progress of the printing, for example, through a System.Windows.Controls. ProgressBar control.

The handler for the WritingProgressCompleted event is used to perform any cleaning up and determine whether the printing completed successfully.

Once the WriteAsync method has been called on the XpsDocumentWriter, the operation can be cancelled by calling the CancelAsync method on the same XpsDocumentWriter. This will stop the printing and fire off the WritingProgressCompleted event on the XpsDocumentWriter.

The WritingCompleted event will pass a WritingCompletedEventArgs object to the handler. The WritingCompletedEventArgs class derives from System.ComponentModel.AsyncCompletedEventArgs and provides information on whether the printing was cancelled or whether an error occurred. This information can then be relayed to the user if required.

Note

The objects being printed should not be released until printing has completed.

The Code

The following code demonstrates how to print a large FixedDocument, in an asynchronous manner, allowing the user to cancel the printing process. A large FixedDocument is generated and stored in a DocumentViewer. The Print button on the document viewer is overridden, executing the custom printing progres, starting with presenting the user with a PrintDialog. Once the PrintDialog is done with, a mask is displayed over the window, graying out the window and displaying a progress bar, which shows the current progress of the print job, and a button that allows the user to cancel the print job.

The following code defines the content of the application's Window1.xaml file:

<Window
  x:Class="Recipe_07_07.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  mlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="400" Width="600">
 <Grid>

   <DocumentViewer x:Name="dvDocumentViewer">
     <DocumentViewer.CommandBindings>
       <CommandBinding
         Command="ApplicationCommands.Print"
         Executed="DocumentViewer_PrintDocument" />
      </DocumentViewer.CommandBindings>
    </DocumentViewer>

    <Grid
      x:Name="spProgressMask"
      Background="#66000000"
      Visibility="Collapsed">
      <StackPanel
        VerticalAlignment="Center"
        HorizontalAlignment="Center">
        <TextBlock Text="Printing document..." />
        <ProgressBar
          x:Name="pbPrintProgress"
          Minimum="0"
          Maximum="100"
          Value="0"
          Width="100"
          Height="20"
        />
        <Button Content="Cancel" Click="btnCancelPrint_Click" />
      </StackPanel>
    </Grid>
  </Grid>
</Window>

The following code defines the content of the Window1.xaml.cs code-behind file:

using System;
using System.Printing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Documents.Serialization;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Xps;
namespace Recipe_07_07
{
      public partial class Window1 : Window
      {
         public Window1()
         {
             InitializeComponent();

             dvDocumentViewer.Document = CreateMultiPageFixedDocument();
         }

        //Creates a FixedDocument with lots of pages.
        public FixedDocument CreateMultiPageFixedDocument()
        {
          FixedDocument fixedDocument = new FixedDocument();
          fixedDocument.DocumentPaginator.PageSize = new Size(96 * 8.5, 96 * 11);

          //Create a large number of pages so we can see the progress
          //bar and cancel button in action.
          for (int i = 0; i < 1000; i++)
          {
              PageContent pageContent = new PageContent();
              fixedDocument.Pages.Add(pageContent);
              FixedPage fixedPage = new FixedPage();

              //Add a canvas with a TextBlock and a Rectangle as children.
              Canvas canvas = new Canvas();
              fixedPage.Children.Add(canvas);

              TextBlock textBlock = new TextBlock();

              textBlock.Text =
                  string.Format("Page {0} / {1}

This Is Page {0}.",
                                i + 1, 1000);

              textBlock.FontSize = 24;
              canvas.Children.Add(textBlock);

              Rectangle rect = new Rectangle();
              rect.Width = 200;
              rect.Height = 200;
              rect.Fill =
                  new SolidColorBrush(Color.FromArgb(200, 20, 50, 200));
              canvas.Children.Add(rect);

              ((IAddChild)pageContent).AddChild(fixedPage);
        }
return fixedDocument;
    }

    //Present the user with a PrintDialog and use it to
    //obtain a reference to a PrintQueue.
    public PrintQueue GetPrintQueue()
    {
        PrintDialog printDialog = new PrintDialog();

        bool? result = printDialog.ShowDialog();

        if (result.HasValue && result.Value)
        {
             return printDialog.PrintQueue;
        }

        return null;
     }

     //Keep a reference to the XPS document writer we use.
     private XpsDocumentWriter xpsDocumentWriter;

     public void PrintDocumentAsync(FixedDocument fixedDocument)
     {
         //Get a hold of a PrintQueue.
         PrintQueue printQueue = GetPrintQueue();

         //Create a document writer to print to.
         xpsDocumentWriter = PrintQueue.CreateXpsDocumentWriter(printQueue);

         //We want to know when the printing progress has changed so
         //we can update the UI.
         xpsDocumentWriter.WritingProgressChanged +=
             PrintAsync_WritingProgressChanged;

         //We also want to know when the print job has finished, allowing
         //us to check for any problems.
         xpsDocumentWriter.WritingCompleted += PrintAsync_Completed;

         StartLongPrintingOperation(fixedDocument.Pages.Count);

         //Print the FixedDocument asynchronously.
         xpsDocumentWriter.WriteAsync(fixedDocument);
      }

      private void PrintAsync_WritingProgressChanged(object sender,
          WritingProgressChangedEventArgs e)
{
         //Another page of the document has been printed. Update the UI.
         pbPrintProgress.Value = e.Number;
     }

     private void PrintAsync_Completed(object sender,
         WritingCompletedEventArgs e)
     {
         string message;
         MessageBoxImage messageBoxImage;

         //Check to see whether there was a problem with the printing.
         if (e.Error != null)
         {
             messageBoxImage = MessageBoxImage.Error;
             message =
                 string.Format("An error occurred whilst printing.

{0}",
                               e.Error.Message);
         }
         else if (e.Cancelled)
         {
             messageBoxImage = MessageBoxImage.Stop;
             message = "Printing was cancelled by the user.";
         }
         else
         {
            messageBoxImage = MessageBoxImage.Information;
            message = "Printing completed successfully.";
         }

         MessageBox.Show(message, "Recipe_07_07",
                         MessageBoxButton.OK, messageBoxImage);

         StopLongPrintingOperation();
      }

      private void StartLongPrintingOperation(int pages)
      {
          pbPrintProgress.Value = 0;
          pbPrintProgress.Maximum = pages;

          spProgressMask.Visibility = Visibility.Visible;
      }
private void StopLongPrintingOperation()
  {
      spProgressMask.Visibility = Visibility.Collapsed;
  }

  private void DocumentViewer_PrintDocument(object sender, EventArgs e)
  {
      PrintDocumentAsync(CreateMultiPageFixedDocument());
  }

  private void btnCancelPrint_Click(object sender, RoutedEventArgs e)
  {
      //The user has clicked the Cancel button.
      //First ensure we have a valid XpsDocumentWriter.
      if (xpsDocumentWriter != null)
      {
          //Cancel the job.
          xpsDocumentWriter.CancelAsync();
       }
     }
   }
}

Programmatically Create and Save a Simple FixedDocument

Problem

You need to create a simple System.Windows.Documents.FixedDocument consisting of a few pages where each page will display some text. You also need to be able to save this document to disk using an XPS document.

Solution

Create a FixedDocument, and add as many System.Windows.Documents.FixedPage objects as required. The visual content of each page is built up by adding elements as children of the FixedPage. Once the FixedDocument is built, it is saved to disk using an XPS document.

How It Works

A FixedDocument consists of a collection of FixedPage objects. Each FixedPage can contain any number of controls including text, images, custom controls, and so on.

The FixedPage object is derived from FrameworkElement and as such has a child collection of type UIElementCollection.

The Code

The following XAML defines a System.Windows.Window that contains a System.Windows.Controls. DocumentViewer and a System.Windows.Controls.Button. When the Window is loaded, a FixedDocument is created and displayed in the DocumentViewer.

The Button is used to initiate the save progress; it displays a System.Windows.Forms.SaveDialog to the user and allows them to specify where to save the XPS document, which contains the FixedDocument.

<Window
  x:Class="Recipe_07_08.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"
  Height="600"
  Width="800"
  Loaded="Window_Loaded">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="25" />
    </Grid.RowDefinitions>

    <DocumentViewer x:Name="dvDocumentViewer" />

    <Button
      Grid.Row="1"
      Content="Save Document"
      Click="btnSaveDocument_Click"
    />
  </Grid>
</Window>

The following code defines the content of the Window1.xaml.cs file:

using System.IO;
using System.IO.Packaging;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Markup;
using System.Windows.Xps;
using System.Windows.Xps.Packaging;
using Microsoft.Win32;
namespace Recipe_07_08
{
        /// <summary>
        /// Interaction logic for Window1.xaml
        /// </summary>
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }

            //Creates a FixedDocument and places it in the document viewer.
            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                dvDocumentViewer.Document = CreateMultiPageFixedDocument();
            }

            public FixedDocument CreateMultiPageFixedDocument()
            {
                FixedDocument fixedDocument = new FixedDocument();

                //Set the size of each page to be A4 (8.5" x 11").
                Size a4PageSize = new Size(8.5 * 96, 11 * 96);
                fixedDocument.DocumentPaginator.PageSize = a4PageSize;

                //Add 5 pages to the document.
                for (int i = 1; i < 6; i++)
                {

                   PageContent pageContent = new PageContent();
                   fixedDocument.Pages.Add(pageContent);

                   FixedPage fixedPage = new FixedPage();
                   //Create a TextBlock
                   TextBlock textBlock = new TextBlock();
                   textBlock.Margin = new Thickness(10, 10, 0, 0);
                   textBlock.Text = string.Format("Page {0}", i);
                   textBlock.FontSize = 24;
                   //Add the TextBlock to the page.
                   fixedPage.Children.Add(textBlock);
                   //Add the page to the page's content.
                   ((IAddChild)pageContent).AddChild(fixedPage);
                }

                return fixedDocument;
            }
//Handles the click event of the save button defined in markup.
private void btnSaveDocument_Click(object sender, RoutedEventArgs e)
{

    //Show a save dialog and get a file path.
    string filePath = ShowSaveDialog();

    //If we didn't get a path, don't try to save.
    if (string.IsNullOrEmpty(filePath))
    {
        return;
    }

    //Save the document to disk to the given file path.
    SaveDocument(filePath, dvDocumentViewer.Document as FixedDocument);
 }

 //Present the user with a save dialog and return a path to a file.
 private string ShowSaveDialog()
 {
    SaveFileDialog saveFileDialog = new SaveFileDialog();
    saveFileDialog.Filter = "XPS Files | *.xps";
    saveFileDialog.OverwritePrompt = true;
    saveFileDialog.CheckFileExists = false;
    saveFileDialog.DefaultExt = ".xps";

    if (saveFileDialog.ShowDialog(this) == true)
    {
        return saveFileDialog.FileName;
    }

    return null;
}

//Save the document to disk to the given file path.
private void SaveDocument(string fileName, FixedDocument document)
{

    //Delete any existing file.
    File.Delete(fileName);

    //Create a new XpsDocument at the given location.
    XpsDocument xpsDocument =
        new XpsDocument(fileName, FileAccess.ReadWrite,
                        CompressionOption.NotCompressed);

    //Create a new XpsDocumentWriter for the XpsDocument object.
    XpsDocumentWriter xdw = XpsDocument.CreateXpsDocumentWriter(xpsDocument);
//Write the document to the Xps file.
       xdw.Write(document);

       //Close down the saved document.
       xpsDocument.Close();
     }
   }
}

Use Figures and Floaters in a FlowDocument

Problem

You need to create a System.Windows.Documents.FlowDocument, which contains inline content such as images, tables, and so on.

Solution

Use the System.Windows.Documents.Floater and System.Windows.Documents.Figure objects to place rich content inline with your text.

How It Works

Floater and Figure objects allow content to be placed in a FlowDocument that is positioned independently of the document's content flow. Floater and Figure objects are for use only in FlowDocument objects and cannot be used in a System.Windows.Documents.FixedDocument.

There are a few differences between Figure and Floater that need to be considered when choosing which to use in a document. Generally, you will want to use a Figure when you require control over the dimensions and location of the Figure's content. If this level of control isn't required, a Floater will be the better choice.

A Floater cannot be positioned in a document; it will simply be placed wherever space can be made available, whereas a Figure can be placed relatively using anchors or absolutely using pixel offsets.

Figure and Floater also differ in the way they can be sized. A Figure's height and width can be sized relative to a page, column, or content or absolutely using pixel values. For relative sizing, expressions are used to indicate by how much the Figure should be sized and relative to what object; for example, use "0.8 page" to occupy 80 percent of the height or width of a page, "0.5 column" to occupy 50 percent of the height or width of a column, or "0.1 content" to occupy 10 percent of the height or width of the content in which the Figure is placed. For page and content relative sizing, 1.0 is the upper limit of the allowed scaling; for example, "2 page" will simply be treated as "1 page."

Sizing a Floater is limited to the width of the Floater, and this can be set using only an absolute pixel value; in other words, "0.25 column" is not a valid value for the width of a Floater. The width of a Floater is also restrained to the width of a column, with the width of a column being the default width for a Floater. If the specified width is greater than the width of a column, the size is capped at the width of a column.

The final major difference between a Figure and Floater is the way in which the two objects are paginated. A Figure will not paginate, and as such, any content that does not fit will simply be clipped. The content within a Floater, on the other hand, will be split across columns and pages.

The Code

The following XAML code defines a window with a System.Windows.Controls.FlowDocumentReader. Inside the FlowDocumentReader is a sample FlowDocument containing some paragraphs of text, alist, a Figure, and a Floater. Resizing the window will demonstrate the way in which the Floater and Figure behave in terms of flow layout.

<Window
  x:Class="Recipe_07_09.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Recipe_07_09
  Height="300"
  Width="300">
  <Grid>
    <FlowDocumentReader>
      <FlowDocument>
        <Paragraph>
          <Run>This is a simple run of text inside a paragraph.</Run>
          <Run>This is another simple run of text inside a paragraph.</Run>
          <Run>This is another simple run of text inside a paragraph.</Run>
          <Run>This is another simple run of text inside a paragraph.</Run>
        </Paragraph>
        <Paragraph>
          <Run>This is a simple run of text inside another paragraph.</Run>
          <Run>This is another simple run of text inside another paragraph.</Run>
          <Run>This is another simple run of text inside another paragraph.</Run>
          <Run>This is another simple run of text inside another paragraph.</Run>
        </Paragraph>
        <Paragraph>
          <Figure HorizontalAnchor="PageCenter" VerticalAnchor="PageCenter"
                  Background="WhiteSmoke" BorderThickness="2" BorderBrush="Black">
            <Paragraph>This is a simple paragraph inside a Figure.
              This is a simple paragraph inside a Figure.
              This is a simple paragraph inside a Figure.
              This is a simple paragraph inside a Figure.
              This is a simple paragraph inside a Figure.
            </Paragraph>
          </Figure>
        </Paragraph>
        <Paragraph>
          <Bold>This is a line of bold text inside another paragraph.</Bold>
          <Bold>This is another line of bold text inside another paragraph.</Bold>
<Bold>This is another line of bold text inside another paragraph.</Bold>
          <Bold>This is another line of bold text inside another paragraph.</Bold>
        </Paragraph>
        <List>
          <ListItem><Paragraph>This is a list item.</Paragraph></ListItem>
          <ListItem><Paragraph>This is a list item.</Paragraph></ListItem>
          <ListItem><Paragraph>
              <Bold>This is a bold list item.</Bold>
          </Paragraph></ListItem>
        </List>
        <Paragraph>
          <Floater Background="Silver">
            <Paragraph>
              This is a simple paragraph inside a floater.
              This is a simple paragraph inside a floater.
              This is a simple paragraph inside a floater.
              This is a simple paragraph inside a floater.
              This is a simple paragraph inside a floater.
            </Paragraph>
          </Floater>
        </Paragraph>
      </FlowDocument>
    </FlowDocumentReader>
  </Grid>
</Window>

Programmatically Create and Save a FlowDocument

Problem

You need to create a simple System.Windows.Documents.FlowDocument in code and save it to disk as a .xaml file.

Solution

Open an existing XPS document for reading, and extract the document content from the file. The content is then displayed in a System.Windows.Controls.RichTextBox where it can be edited. Once editing is complete, the content is then saved to the XPS document.

How It Works

Loading a document into a document viewer is similar to that shown in recipe 7-6, which loads a System.Windows.Documents.FixedDocument from a System.Windows.Xps.Packaging.XpsDocument. The document is loaded and then placed into a FlowDocumentPaginator. This can then be provided as the content to a RichTextBox where the user can edit the content.

The Code

<Window
  x:Class="Recipe_07_10.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"
  Height="300"
  Width="300"
  Loaded="Window_Loaded">
  <DockPanel>
    <Button
      DockPanel.Dock="Bottom"
      Height="25"
      Content="Save..."
      Click="btnSave_Click"
    />

    <FlowDocumentReader x:Name="fdrViewer" />
  </DockPanel>
</Window>

The following code defines the content of the code-behind file for the previous markup:

using System;
using System.IO;
using System.Windows; using System.Windows.Documents;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Xml;
using Microsoft.Win32;

namespace Recipe_07_10
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// This method handles the click event on the only button
/// in the application's main window. The user is presented
        /// with a dialog in which a file path is chosen. A call
        /// to the save method is then made.
        /// </summary>
        private void btnSave_Click(object sender, RoutedEventArgs e)
        {
            string filePath = ShowSaveDialog();

            if (string.IsNullOrEmpty(filePath))
            {
                return;
            }

            SaveFile(filePath, fdrViewer.Document);
         }

        /// <summary>
        /// Creates and displays a SaveFileDialog allowing the user to
        /// select a location to save the document to.
        /// </summary>
        /// <returns>
        /// When the save dialog is closed via the OK button, the
        /// method returns the chosen file path; otherwise it returns
        /// null.
        /// </returns>
        private string ShowSaveDialog()
        {
            SaveFileDialog saveFileDialog = new SaveFileDialog();
            saveFileDialog.Filter = "XAML Files | *.xaml";
            saveFileDialog.OverwritePrompt = true;
            saveFileDialog.CheckFileExists = false;
            saveFileDialog.DefaultExt = ".xaml";

            if (saveFileDialog.ShowDialog(this) == true)
            {
                return saveFileDialog.FileName;
            }

            return null;
         }

         /// <summary>
         /// Saves a FixedDocument to a .xaml file at the target location.
         /// </summary>
         /// <param name="fileName">
         /// The target location for the document.
         /// </param>
/// <param name="documentSource">
         /// An IDocumentPaginatorSource for the FixedDocument to be saved
         /// to disk.
         /// </param>
         private void SaveFile(string fileName,
                               IDocumentPaginatorSource documentSource)
         {
             XmlTextWriter xmlWriter = null;
             TextWriter writer = null;
             Stream file = null;

             try
             {
                file = File.Create(fileName);
                writer = new StreamWriter(file);

                xmlWriter = new XmlTextWriter(writer);

                // Set serialization mode
                XamlDesignerSerializationManager xamlManager =
                    new XamlDesignerSerializationManager(xmlWriter);

                // Serialize
                XamlWriter.Save(documentSource.DocumentPaginator.Source,
                                xamlManager);

             }
             catch (Exception e)
             {
                 string msg = string.Format("Error occurred during saving.{0}{0}{1}",
                     Environment.NewLine,
                     e.Message);

                 MessageBox.Show(msg,
                                 "Recipe_07_10",
                                 MessageBoxButton.OK,
                                 MessageBoxImage.Error);
             }
             finally
             {
                 if (!ReferenceEquals(xmlWriter, null))
                 {
                     xmlWriter.Close();
                 }
if (!ReferenceEquals(writer, null))
           {
               writer.Close();
           }

           if (!ReferenceEquals(file, null))
           {
               file.Close();
           }
       }
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        //Programmatically create a FlowDocument
        FlowDocument flowDocument = new FlowDocument();

        //Create a new paragraph to add to the document.
        Paragraph paragraph = new Paragraph();

        //Add some text to the paragraph.
        paragraph.Inlines.Add("This is a paragraph.");
        paragraph.Inlines.Add(" This is a paragraph.");
        paragraph.Inlines.Add(" This is a paragraph.");
        paragraph.Inlines.Add(" This is a paragraph.");
        paragraph.Inlines.Add(" This is a paragraph.");
        paragraph.Inlines.Add(" This is a paragraph.");
        paragraph.Inlines.Add(" This is a paragraph.");
        paragraph.Inlines.Add(" This is a paragraph.");

        //Add the paragraph to the document.
        flowDocument.Blocks.Add(paragraph);

        //Create a new figure and add an Ellipse to it.
        Figure figure = new Figure();
        paragraph = new Paragraph();
        Ellipse ellipse = new Ellipse();
        ellipse.Width = 50;
        ellipse.Height = 50;
        ellipse.Fill = Brushes.Red;
        ellipse.StrokeThickness = 2;
        ellipse.Stroke = Brushes.Black;
        paragraph.Inlines.Add(ellipse);

        //Add the figure to a paragraph.
        figure.Blocks.Add(paragraph);
//Insert the figure into a new paragraph.
        flowDocument.Blocks.Add(new Paragraph(Figure));

        //Add a final paragraph
        paragraph = new Paragraph();
        paragraph.Inlines.Add("This is another paragraph.");
        paragraph.Inlines.Add(" This is another paragraph.");
        paragraph.Inlines.Add(" This is another paragraph.");
        paragraph.Inlines.Add(" This is another paragraph.");
        paragraph.Inlines.Add(" This is another paragraph.");
        paragraph.Inlines.Add(" This is another paragraph.");
        paragraph.Inlines.Add(" This is another paragraph.");
        paragraph.Inlines.Add(" This is another paragraph.");

        flowDocument.Blocks.Add(paragraph);

        //Now set the content of the document reader to the
        //new FlowDocument
        fdrViewer.Document = flowDocument;
     }
  }
}

Asynchronously Save a FixedDocument to an XPS File

Problem

You need to write a System.Windows.Documents.FixedDocument to an XPS document file stored on disk. Because of the possibility of large files, the writing needs to be performed asynchronously so as to maintain a responsive UI.

Solution

XPS documents are actually System.Windows.Documents.FixedDocument objects and can be created from a range of object types. Create a System.Windows.Xps.XpsDocument in memory, pointing to a given location on disk. Then create a System.Windows.Xps.XpsDocumentWriter for the document, and write the content to be saved to the XpsDocument using the XpsDocumentWriter. WriteAsync method.

How It Works

Saving content to an XpsDocument on disk is similar to printing content (recipe 7-6). In this instance, we create a new XpsDocument in the required location on a physical storage device using the System.IO.FileAccess.ReadWrite mode. An XpsDocumentWriter is then created for the XpsDocument using the static method XpsDocument.CreateXpsDocumentWriter.

When the WriteAsync method gets called on the XpsDocumentWriter, it will now write to the same XPS file as the XpsDocument with which it was created. By hooking into the various events on the XpsDocumentWriter, the UI can be kept up-to-date, and it allows the user to cancel the saving if required.

The Code

Note

You will need to add a project reference to System.Printing and ReachFramework

Warning

The following example generates an XPS document with 1,000 pages. On a dual-core machine with 2GB RAM, this allows a good period of time for interaction with the progress mask. You may want to reduce the number of pages that are created if you experience performance issues by adjusting the constant value passed to the CreateFixedPageDocument method from Window_Loaded.

<Window
  x:Class="Recipe_07_11.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"
  Height="600"
  Width="800"
  Loaded="Window_Loaded">
  <Grid>
    <DockPanel>
      <Button
        DockPanel.Dock="Bottom"
        Click="btnSave_Click"
        Content="Save As..."
      />
      <DocumentViewer
        x:Name="dvDocumentViewer"
      />
    </DockPanel>

    <Grid
      x:Name="spProgressMask"
      Background="#66000000"
      Visibility="Collapsed">
      <StackPanel
        VerticalAlignment="Center"
        HorizontalAlignment="Center">
<TextBlock Text="Saving document..." />
        <ProgressBar
          x:Name="pbSaveProgress"
          Minimum="0"
          Maximum="100"
          Value="0"
          Width="100"
          Height="20"
        />
        <Button
          Content="Cancel"
          Click="btnCancelSave_Click" />
      </StackPanel>
    </Grid>
  </Grid>
</Window>

The following code defines the content of the Window1.xaml.cs file:

using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Documents.Serialization;
using System.Windows.Markup;
using System.Windows.Xps;
using System.Windows.Xps.Packaging;
using Microsoft.Win32;

namespace Recipe_07_11
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        private XpsDocumentWriter xdw = null;

        public Window1()
        {
            InitializeComponent();
        }

        private void btnSave_Click(object sender, RoutedEventArgs e)
        {
            //Present the user with a save dialog, getting the path
            //to a file where the document will be saved.
            SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = ".xps|*.xps";
            saveFileDialog.OverwritePrompt = true;
            saveFileDialog.Title = "Save to Xps Document";

            //If the user cancelled the dialog, bail.
            if (saveFileDialog.ShowDialog(this) == false)
            {
                return;
            }

            //Save the document.
            SaveDocument(saveFileDialog.FileName,
                dvDocumentViewer.Document as FixedDocument);
        }

        private void SaveDocument(string fileName, FixedDocument document)
        {
            //Delete any existing file.
            File.Delete(fileName);

            //Create a new XpsDocument at the given location.
            XpsDocument xpsDocument =
                new XpsDocument(fileName, FileAccess.ReadWrite);

            //Create a new XpsDocumentWriter for the XpsDocument object.
            xdw = XpsDocument.CreateXpsDocumentWriter(xpsDocument);

            //We want to be notified of when the progress changes.
            xdw.WritingProgressChanged +=
                delegate(object sender, WritingProgressChangedEventArgs e)
                {   //Update the value of the progress bar.
                    pbSaveProgress.Value = e.Number;
                };

          //We want to be notified of when the operation is complete.
          xdw.WritingCompleted +=
              delegate(object sender, WritingCompletedEventArgs e)
              {
                  //We're finished with the XPS document, so close it.
                  //This step is important.
                  xpsDocument.Close();

                  string msg = "Saving complete.";

                  if (e.Error != null)
                  {
                      msg =
string.Format("An error occurred whilst " +
                                "saving the document.

{0}",
                                e.Error.Message);
               }
               else if (e.Cancelled)
               {
                   //Delete the incomplete file.
                   File.Delete(fileName);

                   msg =
                       string.Format("Saving cancelled by user.");
               }

               //Inform the user of the print operation's exit status.
               MessageBox.Show(msg,
                               "Recipe_07_11",
                               MessageBoxButton.OK,
                               MessageBoxImage.Information);

               spProgressMask.Visibility = Visibility.Collapsed;
            };

         /Show the long operation mask with the Cancel button and progress bar.
         spProgressMask.Visibility = Visibility.Visible;
         pbSaveProgress.Maximum = document.Pages.Count;
         pbSaveProgress.Value = 0;

         //Write the document to the XPS file asynchronously.
         xdw.WriteAsync(document);
     }

     private void btnCancelSave_Click(object sender, RoutedEventArgs e)
     {
         //When the 'Cancel' button is clicked, we want to try and
         //stop the save process.
         if (xdw != null)
             xdw.CancelAsync();
     }

     private void Window_Loaded(object sender, RoutedEventArgs e)
     {
         //Load the DocumentViewer with a simple FixedDocument.
         //A large number of pages are generated so that the progress
         //of the printing is slow enough to be observed.
         dvDocumentViewer.Document = CreateFixedPageDocument(1000);
     }
private FixedDocument CreateFixedPageDocument(int numberOfPages)
{
    // Create a FixedDocument
    FixedDocument fixedDocument = new FixedDocument();
    fixedDocument.DocumentPaginator.PageSize = new Size(96 * 8.5, 96 * 11);

    for (int i = 0; i < numberOfPages; i++)
    {
        PageContent pageContent = new PageContent();
        fixedDocument.Pages.Add(pageContent);
        FixedPage fixedPage = new FixedPage();
        TextBlock textBlock = new TextBlock();
        textBlock.Text = string.Format("Page {0}", i);
        textBlock.FontSize = 24;
        fixedPage.Children.Add(textBlock);
        ((IAddChild)pageContent).AddChild(fixedPage);
     }

     return fixedDocument;

    }
  }
}

Display a Document

Problem

You need to display a System.Windows.Documents.FixedDocument or System.Windows.Documents.FlowDocument in your application.

Solution

Create a new instance of a System.Windows.Xps.Packaging.XpsDocument, passing in the path to the XPS file you want to load. A FixedDocumentSequence can then be retrieved from the XpsDocument and used to display the content of the file in a System.Windows.Controls.DocumentViewer in your application.

How It Works

Several controls are provided for viewing documents in WPF. The control you use for displaying a document will depend on the type of document being displayed and the functionality you want to offer the user. Each of the viewers provides built-in printing, text searching, and scaling. When displaying a FixedDocument, the choice is limited to a System.Windows.Controls.

DocumentViewer, whereas you have three options when it comes to displaying a FlowDocument. The possible controls are as follows:

  • System.Windows.Controls.FlowDocumentPageViewer

  • System.Windows.Controls.FlowDocumentReader

  • System.Windows.Controls.FlowDocumentScrollViewer

The FlowDocumentReader is the most heavyweight of the three and allows the viewer to dynamically switch viewing modes. Three viewing modes are available; one displays the document a page at a time (single page), another displays two pages side by side (book reading format), and the last displays the document as a single, continuous page that the viewer scrolls through.

Should the requirements be such that the mode doesn't need to be dynamic, the FlowDocumentPageViewer can be used for displaying the document in terms of pages (single or book reading format), and the FlowDocumentScrollViewer can be used to display the document as a single scrollable page.

The Code

The following XAML defines a System.Windows.Window displaying a System.Windows.Controls. TabControl. The TabControl contains two System.Windows.Controls.TabItem objects, one of which contains a DocumentViewer for displaying a FixedDocument and the other contains a FlowDocumentReader for displaying a FlowDocument. Both TabItem elements also contain a Button, used for selecting a file to open and display in the appropriate viewer control.

<Window
  x:Class="Recipe_07_12.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"
  Height="600"
  Width="800">
  <TabControl>
    <TabItem Header="Fixed Document">
      <DockPanel>
        <Button
          Height="24"
          Margin="5"
          DockPanel.Dock="Bottom"
          Content="Open..."
          Click="btnOpenFixedDoc_Click"
        />
        <DocumentViewer x:Name="dvDocumentViewer" />
      </DockPanel>
    </TabItem>
    <TabItem Header="Flow Document">
      <DockPanel>
        <Button
Height="24"
                 Margin="5"
                 DockPanel.Dock="Bottom"
                 Content="Open..."
                 Click="btnOpenFlowDoc_Click"
              />
              <FlowDocumentPageViewer x:Name="fdv"/>
           </DockPanel>
       </TabItem>
    </TabControl>
</Window>

The following code defines the content of the Window1.xaml.cs file:

using System;
using System.IO;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Markup;
using System.Windows.Xps.Packaging;
using System.Xml;
using Microsoft.Win32;

namespace Recipe_07_12
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        //Handles the click event of the 'Open...'
        //button for the fixed document viewer.
        private void btnOpenFixedDoc_Click(object sender,
            RoutedEventArgs e)
        {
            string filePath =
                GetFileName("XPS Document (*.xps)|*.xps");

            if (string.IsNullOrEmpty(filePath))
            {
                ShowFileOpenError(filePath);
                return;
            }
IDocumentPaginatorSource documentSource =
                   OpenFixedDocument(filePath);

               if (documentSource == null)
               {
                   ShowFileOpenError(filePath);
               }

               dvDocumentViewer.Document = documentSource;
            }

            //Handles the click event of the 'Open...'
            //button for the flow document viewer.
            private void btnOpenFlowDoc_Click(object sender,
                RoutedEventArgs e)
            {
                string filePath =
                    GetFileName("XAML Document (*.xaml)|*.xaml");

                if (string.IsNullOrEmpty(filePath))
                {

                    ShowFileOpenError(filePath);
                    return;
                }

                FlowDocument flowDocument = OpenFlowDocument(filePath);

                if (flowDocument == null)
                {
                    ShowFileOpenError(filePath);
                    return;
                }

                fdv.Document = flowDocument;
            }

            //Presents the user with an open file dialog and returns
            //the path to any file they select to open.
            private string GetFileName(string filter)
            {
                //First get the file to be opened
                OpenFileDialog openFileDialog = new OpenFileDialog();
                openFileDialog.Filter = filter;
                openFileDialog.Multiselect = false;
                openFileDialog.CheckFileExists = true;
                openFileDialog.CheckPathExists = true;
if (openFileDialog.ShowDialog() == true)
               {
                  return openFileDialog.FileName;
               }

               return null;
           }

           private IDocumentPaginatorSource OpenFixedDocument(
               string fileName)
           {
                try
                {
                    //Load the XpsDocument into memory.
                    XpsDocument document =
                        new XpsDocument(fileName, FileAccess.Read);

                    if (document == null)
                    {
                        return null;
                    }

                    //Get an IDocumentPaginatorSource for the document.
                    return document.GetFixedDocumentSequence();
                }
                catch (Exception)
                {
                    return null;
                }
             }

             private FlowDocument OpenFlowDocument(string fileName)
             {
                 Stream file = null;
                 TextReader reader = null;
                 XmlTextReader xmlReader = null;

                 try
                 {
                     //Load the file into memory.
                     file = File.OpenRead(fileName);
                     reader = new StreamReader(file);
                     //Create an XmlTextReader to use with
                     //the XamlReader below.
                     xmlReader = new XmlTextReader(reader);
//Parse the XAML file and load the FlowDocument.
               return XamlReader.Load(xmlReader) as FlowDocument;
            }
            catch (Exception)
            {
                return null;
            }
            finally
            {
                if (file != null)
                    file.Dispose();

                if (reader != null)
                    reader.Dispose();

                if (xmlReader != null)
                    xmlReader.Close();
          }
     }

     //Display a message if the file cannot be opened.
     private void ShowFileOpenError(string filePath)
     {
        string msg = string.Format("Unable to open " + filePath);
        MessageBox.Show(msg, "Recipe 7-12",
            MessageBoxButton.OK, MessageBoxImage.Error);
        return;
     }
   }
}

Annotate a Document with Sticky Notes

Problem

You are displaying a System.Windows.Documents.FixedDocument System.Windows.Documents. FlowDocument in your application, and you want to allow the user to annotate the document with sticky notes, just as you would with a hard-copy document.

Solution

Adding sticky notes to a document allows you to annotate sections of the document with text notes.

How It Works

Annotations are widely used on hard-copy documents and as such have been included in the document support in .NET 3.0. Sticky notes are a type of annotation that can be applied to content being displayed in any of the following controls:

  • DocumentViewer

  • FlowDocumentPageViewer

  • FlowDocumentReader

  • FlowDocumentScrollViewer

The Annotation APIs in the framework provide all the functionality to manage annotations but provide no entry point on the UI to do so. This requires a little extra work on your part. To make life easier, the System.Windows.Annotations.AnnotationService provides a collection of commands to manage the annotations in one of the previous controls. For the purpose of this recipe, the management of annotations will be handled in code.

A context menu item is added to the FlowDocumentViewer, which is where the handler will create the sticky note. This method retrieves the current username and then creates a new sticky note for the current text selection in the document viewer.

The newly created sticky note is placed into the AnnotationStore from where it can be retrieved later. (See recipe 7-15 for more information on AnnotationStore.)

The Code

The following XAML defines a Window containing a System.Windows.Controls.TabControl with two System.Windows.Controls.TabItem elements. The first TabItem contains a System.Windows.Controls.DocumentViewer, and the second contains a System.Windows.Controls.FixedDocumentReader.

When the Window is loaded, a simple FixedDocument and a simple FlowDocument are created and placed into the DocumentViewer and FlowDocumentReader, respectively. Adding sticky notes is as simple as selecting some text, right-clicking, and selecting the Add Comment menu item.

<Window
  x:Class="Recipe_07_13.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"
  Height="300"
  Width="300"
  Loaded="Window_Loaded">
  <TabControl>
    <TabItem Header="Fixed Document">
      <DocumentViewer x:Name="xdv">
        <DocumentViewer.ContextMenu>
          <ContextMenu>
            <MenuItem
              Header="Add Comment..."
Click="Xdv_AddComment_Click" />
           </ContextMenu>
         </DocumentViewer.ContextMenu>
       </DocumentViewer>
     </TabItem>

     <TabItem Header="Flow Document">
       <FlowDocumentPageViewer x:Name="fdv">
         <FlowDocumentPageViewer.ContextMenu>
           <ContextMenu>
             <MenuItem
               Header="Add Comment..."
               Click="Fdv_AddComment_Click" />
           </ContextMenu>
         </FlowDocumentPageViewer.ContextMenu>
       </FlowDocumentPageViewer>
     </TabItem>
   </TabControl>
</Window>

The following code defines the content of the Window1.xaml.cs file:

using System;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Annotations;
using System.Windows.Annotations.Storage;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Xps.Packaging;

namespace Recipe_07_13
{

    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        //FixedDoxument specifics
        AnnotationService fixedAnnotationService;
        AnnotationStore fixedAnntationStore;
        MemoryStream fixedAnnotationBuffer;

        //FlowDocument specifics
        AnnotationService flowAnnotationService;
        AnnotationStore flowAnntationStore;
        MemoryStream flowAnnotationBuffer;
public Window1()
{
    InitializeComponent();
}

//When the Window is loaded, we want to get hold of some
//test document to try the sticky notes on, then
//start up the annotation services that make it possible.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
    //Load in our sample FixedDocument
    LoadFixedDocument();
    //Create a new FlowDocument
    fdv.Document = CreateFlowDocument();

    //Start the annotation services
    StartFixedDocumentAnnotations();
    StartFlowDocumentAnnotations();
}

//Handles the user clicking the Add Comment context menu item
//on the document viewer in the FixedDocument tab.
private void Xdv_AddComment_Click(object sender, RoutedEventArgs e)
{

    //Get the current user's name and
    //use as the comment's author
    string userName = System.Environment.UserName;

    //The AnnotationHelper.CreateTextStickyNoteForSelection method
    //will throw an exception if no text is selected.
    try
    {
        AnnotationHelper.CreateTextStickyNoteForSelection(
            fixedAnnotationService, userName);
    }
    catch (InvalidOperationException)
    {
        MessageBox.Show("Please select some text to annotate.");
    }
}

//Handles the user clicking the Add Comment context menu item
//on the document viewer in the FlowDocument tab.
private void Fdv_AddComment_Click(object sender, RoutedEventArgs e)
{
    //Get the current user's name as the author
    string userName = System.Environment.UserName;
//The AnnotationHelper.CreateTextStickyNoteForSelection method
    //will throw an exception if no text is selected.
    try
    {
        AnnotationHelper.CreateTextStickyNoteForSelection(
            flowAnnotationService, userName);
    }
    catch (InvalidOperationException)
    {
        MessageBox.Show("Please select some text to annotate.");
    }
}

private void StartFixedDocumentAnnotations()
{
    //Create a new annotation service for the fixed document viewer.
    fixedAnnotationService = new AnnotationService(xdv);

    //Open a stream for our annotation store.
    fixedAnnotationBuffer = new MemoryStream();

    //Create an AnnotationStore using the stream.
    fixedAnntationStore = new XmlStreamStore(fixedAnnotationBuffer);

    //Enable the AnnotationService against the new annotation store.
    fixedAnnotationService.Enable(fixedAnntationStore);
}

private void StartFlowDocumentAnnotations()
{
    //Create a new annotation service for the fixed document viewer.
    flowAnnotationService = new AnnotationService(fdv);

    //Open a stream for our annotation store.
    flowAnnotationBuffer = new MemoryStream();

    //Create an AnnotationStore using the stream.
    flowAnntationStore = new XmlStreamStore(flowAnnotationBuffer);

    //Enable the AnnotationService against the new annotation store.
    flowAnnotationService.Enable(flowAnntationStore);
}

//Create a simple FlowDocument that can be used for testing out
//sticky notes.
private FlowDocument CreateFlowDocument()
{
    FlowDocument flowDocument = new FlowDocument();
    Paragraph paragraph = new Paragraph();
    paragraph.FontSize = 12;
    paragraph.Foreground = Brushes.Black;
    paragraph.FontWeight = FontWeights.Bold;
    paragraph.Inlines.Add(new Run("This is a FlowDocument."));

    flowDocument.Blocks.Add(paragraph);

    paragraph = new Paragraph();
    paragraph.FontWeight = FontWeights.Normal;
    paragraph.Inlines.Add(
        new Run("This is a paragraph in the FlowDocument."));

    flowDocument.Blocks.Add(paragraph);

    return flowDocument;
}

//An XPS document is loaded and displayed in the document viewer,
//ready for annotating.
private void LoadFixedDocument()
{
    string documentPath =
        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
        + "\SampleDocument\FixedDocument.xps";

    //Create a URI for the file path.
    Uri documentUri = new Uri(documentPath, UriKind.Absolute);

    XpsDocument xpsDocument = null;

    try
    {
        //Attempts to open the specified XPS document with
        //read and write permission.
        xpsDocument = new XpsDocument(documentPath, FileAccess.ReadWrite);
    }
    catch (Exception)
    {
        //You may want to handle any errors that occur during
        //the loading of the XPS document. For example, an
        //UnauthorizedAccessException will be thrown if the
        //file is marked as read-only.
    }
//If the document is null, it's not a valid XPS document.
         if (xpsDocument == null)
         {
             //You may want to log an error here.
             return;
         }

         //Get the FixedDocumentSequence of the loaded document.
         FixedDocumentSequence fixedDocumentSequence
             = xpsDocument.GetFixedDocumentSequence();

         //If the document's FixedDocumentSequence is not found,
         //the document is corrupt.
         if (fixedDocumentSequence == null)
         {
             //Handle as required.
             return;
         }

         //Load the document's FixedDocumentSequence into
         //the DocumentViewer control.
         xdv.Document = fixedDocumentSequence;
      }
   }
}

Use Highlighting in a Document

Problem

You need to allow a user to highlight sections of a document.

Solution

Use the System.Windows.Annotations.AnnotationHelper to create highlighted sections of text in content displayed in a document viewer.

How It Works

Highlighting is another form of annotating a document and is performed in much the same way as creating sticky notes. The AnnotationHelper class provides a method for applying highlighting to a selection of text in a document being presented in a document viewer through the CreateHighlightForSelection method. This method takes the following parameters: a System.Windows.Annotations.AnnotationService object on which to create the highlight, a System.String giving the name of the author; and a System.Windows.Media.Brush that is used as the highlight color.

As you would imagine, removing highlighting from sections of text is just as simple. This time, you use the ClearHighlightForSelection method of the AnnotationHelper, passing in the AnnotationService on which the highlight is to be cleared. This method will clear only the highlighting applied to the selected text in the document viewer. This allows you to add or remove highlights on a character-by-character basis or to clear all the highlights in a selection of text where only some text is selected.

Calling either of these two methods when there is no text selected will cause a System.InvalidOperationException to be thrown.

The Code

The following XAML defines a window containing a System.Windows.Controls.TabControl with two System.Windows.Controls.TabItem elements. The first TabItem contains a System.Windows.Controls.DocumentViewer, and the second contains a System.Windows.Controls.FixedDocumentReader.

When the Window is loaded, a simple FixedDocument and a simple FlowDocument are created and placed into the DocumentViewer and the FlowDocumentReader, respectively. Adding sticky notes is as simple as selecting some text, right-clicking, and selecting the Add Highlight menu item. Removing highlights is performed in the same manner, although you select the Clear Highlight(s) menu item.

<Window
  x:Class="Recipe_07_14.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"
  Height="800"
  Width="600"
  Loaded="Window_Loaded">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="20" />
    </Grid.RowDefinitions>

    <TabControl>
      <TabItem Header="Fixed Document">
        <DocumentViewer x:Name="xdv">
          <DocumentViewer.ContextMenu>
            <ContextMenu>
              <MenuItem
                Header="Add Highlight"
                Click="DocumentViewer_AddHighlight"
                Tag="fixed" />
              <MenuItem
                Header="Clear Highlight(s)"
                Click="DocumentViewer_ClearHighlight"
                Tag="fixed" />
</ContextMenu>
                 </DocumentViewer.ContextMenu>
               </DocumentViewer>
             </TabItem>

             <TabItem Header="Flow Document">
               <FlowDocumentPageViewer x:Name="fdv">
                 <FlowDocumentPageViewer.ContextMenu>
                   <ContextMenu>
                     <MenuItem
                       Header="Add Highlight"
                       Click="DocumentViewer_AddHighlight"
                       Tag="flow" />
                     <MenuItem
                       Header="Clear Highlight(s)"
                       Click="DocumentViewer_ClearHighlight"
                       Tag="flow" />
                   </ContextMenu>
                 </FlowDocumentPageViewer.ContextMenu>
               </FlowDocumentPageViewer>
             </TabItem>
           </TabControl>
         </Grid>
       </Window>

The following code defines the content of the Window1.xaml.cs file:

using System;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Annotations;
using System.Windows.Annotations.Storage;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Xps.Packaging;

namespace Recipe_07_14
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {

        //FixedDoxument specifics
        AnnotationService fixedAnnotationService;
AnnotationStore fixedAnntationStore;
      MemoryStream fixedAnnotationBuffer;

      //FlowDocument specifics
      AnnotationService flowAnnotationService;
      AnnotationStore flowAnntationStore;
      MemoryStream flowAnnotationBuffer;

      XpsDocument xpsDocument;

      public Window1()
      {
          InitializeComponent();
          //Fire up the annotation services.
         StartFixedDocumentAnnotations();
         StartFlowDocumentAnnotations();
      }

      private void Window_Loaded(object sender, RoutedEventArgs e)
      {
          //Populate the two document viewers.
          LoadFixedDocument();
          fdv.Document = CreateFlowDocument();
      }

      private void StartFixedDocumentAnnotations()
      {
          //Create a new annotation service for the fixed document viewer.
          fixedAnnotationService = new AnnotationService(xdv);

          //Open a stream for our annotation store.
          fixedAnnotationBuffer = new MemoryStream();

         //Create an AnnotationStore using the stream.
         fixedAnntationStore = new XmlStreamStore(fixedAnnotationBuffer);

         //Enable the AnnotationService against the new annotation store.
         fixedAnnotationService.Enable(fixedAnntationStore);
     }

     private void StartFlowDocumentAnnotations()
     {

         //Create a new annotation service for the fixed document viewer.
         flowAnnotationService = new AnnotationService(fdv);

         //Open a stream for our annotation store.
         flowAnnotationBuffer = new MemoryStream();
//Create an AnnotationStore using the stream.
    flowAnntationStore = new XmlStreamStore(flowAnnotationBuffer);

    //Enable the AnnotationService against the new annotation store.
    flowAnnotationService.Enable(flowAnntationStore);
}

//This method is called when the Add Highlight context menu is
//clicked by the user on either of the two document viewer controls.
private void DocumentViewer_AddHighlight(object sender,
    RoutedEventArgs e)
{
    //Work out which document viewer we are dealing with
    //and get the appropriate store.
    string tag = ((MenuItem)sender).Tag.ToString();

    AnnotationService annotationService =
        tag == "fixed"
        ? fixedAnnotationService
        : flowAnnotationService;

    //Get the current user's name as the author
    string userName = System.Environment.UserName;

    try
    {
        //Creates a yellow highlight
        AnnotationHelper.CreateHighlightForSelection(
            annotationService, userName, Brushes.Yellow);
    }
    catch (InvalidOperationException)
    {
        MessageBox.Show("Please select some text to highlight.");
    }
}

private void DocumentViewer_ClearHighlight(object sender, RoutedEventArgs e)
{
    //Work out which document viewer we are dealing with
    //and get the appropriate store.
    string tag = ((MenuItem)sender).Tag.ToString();

    AnnotationService annotationService =
        tag == "fixed"
        ? fixedAnnotationService
        : flowAnnotationService;
try
    {
        //Clear the selected text of any highlights.
        AnnotationHelper.ClearHighlightsForSelection(
            annotationService);
    }
    catch (InvalidOperationException)
    {
        MessageBox.Show("Please select some text to clear.");
    }
}

//Creates a simple FlowDocument containing text that can be
//highlighted.
private FlowDocument CreateFlowDocument()
{
    FlowDocument flowDocument = new FlowDocument();
    Paragraph paragraph = new Paragraph();
    paragraph.FontSize = 12;
    paragraph.Foreground = Brushes.Black;
    paragraph.FontWeight = FontWeights.Bold;
    paragraph.Inlines.Add(new Run("This is a FlowDocument."));

    flowDocument.Blocks.Add(paragraph);

    paragraph = new Paragraph();
    paragraph.FontWeight = FontWeights.Normal;
    paragraph.Inlines.Add(
        new Run("This is a paragraph in the FlowDocument."));

    flowDocument.Blocks.Add(paragraph);

    return flowDocument;
}

//An XPS document is loaded and displayed in the document viewer,
//ready for annotating.
private void LoadFixedDocument()
{
    string documentPath =
        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
        + "\SampleDocument\FixedDocument.xps";

    //Create a URI for the file path.
    Uri documentUri = new Uri(documentPath, UriKind.Absolute);

    xpsDocument = null;
try
         {
             //Attempts to open the specified XPS document with
             //read and write permission.
             xpsDocument = new XpsDocument(documentPath,
                 FileAccess.ReadWrite);
         }
         catch (Exception)
         {
             //You may want to handle any errors that occur during
             //the loading of the XPS document. For example an
             //UnauthorizedAccessException will be thrown if the
             //file is marked as read-only.
         }

         //If the document is null, it's not a valid XPS document.
         if (xpsDocument == null)
             return; //Handle as required.

         //Get the FixedDocumentSequence of the loaded document.
         FixedDocumentSequence fixedDocumentSequence
             = xpsDocument.GetFixedDocumentSequence();

         //If the document's FixedDocumentSequence is not found,
         //the document is corrupt.
         if (fixedDocumentSequence == null)
             return; //Handle as required.

         //Load the document's FixedDocumentSequence into
         //the DocumentViewer control.
         xdv.Document = fixedDocumentSequence;
      }
   }
}

Load and Save User-Defined Annotations

Problem

You need to display an XPS document-based System.Windows.Documents.FixedDocument in your application, including any annotations that may be in the document. Any new annotations made on the document also need to be persisted.

Solution

The annotation framework in WPF allows different methods for serializing annotations on a document. Using XML to store the annotations, you can add or load them from a System.Windows.Xps.Packaging.XpsDocument stored on disk.

How It Works

Annotations are stored and managed using a System.Windows.Annotations.Storage.AnnotationStore. The AnnotationStore class is an abstract class and implemented by System.Windows.Annotations.Storage.XmlStreamStore, an XML-based data store for annotation data. With an XmlStreamStore, it is possible to add annotations to an XpsDocument simply by adding a new System.IO.Packaging.PackagePart to the System.IO.Packaging.Package, containing the annotations.

For the purpose of this example, commands found in the System.Windows.Annotations namespace have been used to add and remove annotations from the document. This ensures that the provided code is more focused on the persistence of the annotations, rather than their creation/deletion.

Note

You will need to add references to the System.Printing and ReachFramework assemblies in your project for this example.

Warning

Each time this sample is built, the sample XPS document supplied with the source code is copied to the output directory, overwriting any existing one. This means that any saved annotations will be lost after each build. Should you want to use a different document, just print a document on your computer, and select Microsoft XPS Document Writer as your target printer.

The Code

<Window
  x:Class="Recipe_07_15.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:a="clr-namespace:System.Windows.Annotations;assembly=PresentationFramework"
  Title="Window1"
  Height="600"
  Width="800"
  Closed="Window_Closed">
  <DockPanel>

    <Grid DockPanel.Dock="Bottom">
      <Grid.ColumnDefinitions>
<ColumnDefinition Width="0.5*" />
        <ColumnDefinition Width="0.5*" />
      </Grid.ColumnDefinitions>

      <Button
        Content="Open XPS..."
        Click="btnOpenXps_Click"
      />

      <Button
        Content="Save Annotations"
        Click="btnSaveXps_Click"
        Grid.Column="1"
      />
    </Grid>

    <DocumentViewer x:Name="dvViewer">
      <DocumentViewer.ContextMenu>
        <ContextMenu>
          <MenuItem
            Header="Add Comment"
            Command="a:AnnotationService.CreateTextStickyNoteCommand"
          />

          <MenuItem
            Header="Add Highlight"
            Command="a:AnnotationService.CreateHighlightCommand"
          />

          <Separator />

          <MenuItem
            Command="a:AnnotationService.DeleteStickyNotesCommand"
            Header="Remove Notes"
          />

          <MenuItem
            Command="a:AnnotationService.ClearHighlightsCommand"
            Header="Remove Highlight"
          />
        </ContextMenu>
      </DocumentViewer.ContextMenu>
    </DocumentViewer>
  </DockPanel>
</Window>

The following code defines the content of the Window1.xaml.cs file:

using System;
using System.IO;
using System.IO.Packaging;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Annotations;
using System.Windows.Annotations.Storage;
using System.Windows.Documents;
using System.Windows.Xps.Packaging;
using Microsoft.Win32;

namespace Recipe_07_15
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        private AnnotationService fixedAnnotationService;
        private AnnotationStore fixedAnntationStore;
        private Stream fixedAnnotationBuffer;
        private Uri documentUri;
        private Package xpsPackage;
        private XpsDocument xpsDocument;

        private bool hasOpenDocument;

        private const string fixedDocumentSequenceContentType =
            "application/vnd.ms-package.xps-fixeddocumentsequence+xml";

        private const string annotRelsType =
              "http://schemas.microsoft.com/xps/2005/06/annotations";

        private const string annotContentType =
              "application/vnd.ms-package.annotations+xml";

        public Window1()
        {

            InitializeComponent();

            hasOpenDocument = false;
        }

        //Handles the Click event raised by the
        //Open button, defined in markup.
private void btnOpenXps_Click(object sender, RoutedEventArgs e)
        {
            CloseFixedDocument();

            LoadFixedDocument();
        }

        //Handles the Click event raised by the
        //Save button, defined in markup.
        private void btnSaveXps_Click(object sender, RoutedEventArgs e)
        {
            SaveAnnotations();
        }

        //Handles the Closed event, raised by the window
        //defined in markup, clearing up.
        private void Window_Closed(object sender, EventArgs e)
        {
            StopFixedDocumentAnnotations();
        }

        //Closes an open document and tidies up.
        private void CloseFixedDocument()
        {
            if (hasOpenDocument)
            {
                StopFixedDocumentAnnotations();

                PackageStore.RemovePackage(documentUri);

                xpsDocument.Close();
                xpsDocument = null;

                xpsPackage.Close();
                xpsPackage = null;
            }
        }

        //Presents the user with an OpenFileDialog, used to get a path
        //to the XPS document they want to open. If this succeeds,
        //the XPS document is loaded and displayed in the document viewer,
        //ready for annotating.
        private void LoadFixedDocument()
        {

            //Get a path to the file to be opened.
            string fileName = GetDocumentPath();
            //If we didn't get a valid file path, we're done.
//You might want to log an error here.
        if (string.IsNullOrEmpty(fileName))
        {
            return;
        }

        //Create a URI for the file path.
        documentUri = new Uri(fileName, UriKind.Absolute);

        try
        {
            //Attempts to open the specified XPS document with
            //read and write permission.
            xpsDocument = new XpsDocument(fileName, FileAccess.ReadWrite);
        }
        catch (Exception)
        {
            //You may want to handle any errors that occur during
            //the loading of the XPS document. For example, an
            //UnauthorizedAccessException will be thrown if the
            //file is marked as read-only.
        }

        //Get the document's Package from the PackageStore.
        xpsPackage = PackageStore.GetPackage(documentUri);

        //If either the package or document are null, the
        //document is not valid.
        if ((xpsPackage == null) || (xpsDocument == null))
        {
             //You may want to log an error here.
             return;
        }

        //Get the FixedDocumentSequence of the loaded document.
        FixedDocumentSequence fixedDocumentSequence
            = xpsDocument.GetFixedDocumentSequence();

        //If the document's FixedDocumentSequence is not found,
        //the document is corrupt.
        if (fixedDocumentSequence == null)
        {
            //Handle as required.
            return;
        }
//Load the document's FixedDocumentSequence into
    //the DocumentViewer control.
    dvViewer.Document = fixedDocumentSequence;

    //Enable user annotations on the document.
    StartFixedDocumentAnnotations();

    hasOpenDocument = true;
}

//Present the user with an OpenFileDialog, allowing
//them to select a file to open. If a file is selected,
//return the path to the file; otherwise, return an empty
//string.
private string GetDocumentPath()
{
    OpenFileDialog openFileDialog = new OpenFileDialog();
    openFileDialog.Filter = "XPS Document | *.xps";
    openFileDialog.Multiselect = false;
    openFileDialog.CheckFileExists = true;
    openFileDialog.CheckPathExists = true;

    openFileDialog.InitialDirectory =
        Path.GetFullPath(Assembly.GetExecutingAssembly().Location);

    string result = string.Empty;

    if (openFileDialog.ShowDialog(this) == true)
    {
        result = openFileDialog.FileName;
    }

    return result;
}

//Saves the document's annotations by flushing their buffers.
//The package is also flushed so that the changes are persisted
//to disk.
private void SaveAnnotations()
{
    //Check that we have a valid fixed annotation service.
    if (fixedAnnotationService != null
        && fixedAnnotationService.IsEnabled)
    {
        fixedAnnotationService.Store.Flush();
        fixedAnnotationBuffer.Flush();
    }
if (xpsPackage != null)
    {
        xpsPackage.Flush();
    }
}

private void StartFixedDocumentAnnotations()
{
    //If there is no AnnotationService yet, create one.
    if (fixedAnnotationService == null)
    {
        fixedAnnotationService = new AnnotationService(dvViewer);
    }

    //If the AnnotationService is currently enabled, disable it
    //because you'll need to reenable it with a new store object.
    if (fixedAnnotationService.IsEnabled)
    {
        fixedAnnotationService.Disable();
    }

    //Open a stream to the file for storing annotations.
    fixedAnnotationBuffer =
        GetAnnotationPart(GetFixedDocumentSequenceUri()).GetStream();

    //Create a new AnnotationStore using the file stream.
    fixedAnntationStore = new XmlStreamStore(fixedAnnotationBuffer);

    //Enable the AnnotationService using the new store object.
    fixedAnnotationService.Enable(fixedAnntationStore);
}

//When closing the application, or just the document it is
//important to close down the existing annotation service,
//releasing any resources. Note that the annotation service
//is stopped without saving changes.
public void StopFixedDocumentAnnotations()
{
    //If the AnnotationStore is active, flush and close it.
    if ((fixedAnnotationService != null)
        && fixedAnnotationService.IsEnabled)
    {
        fixedAnnotationService.Store.Dispose();
        fixedAnnotationBuffer.Close();
    }
//If the AnnotationService is active, shut it down.
   if (fixedAnnotationService != null)
   {
       if (fixedAnnotationService.IsEnabled)
       {
         fixedAnnotationService.Disable();
       }

       fixedAnnotationService = null;
   }
}

//Searches the parts of a document, looking for the
//FixedDocumentSequence part. If the part is found,
//its URI is returned; otherwise, null is returned.
private Uri GetFixedDocumentSequenceUri()
{
    Uri result = null;
    PackagePart packagePart;

    //Get the FixedDocumentSequence part from the Package.
    packagePart = xpsPackage.GetParts().Single<PackagePart>(
        part =>
            part.ContentType == fixedDocumentSequenceContentType);

    //If we found the part, note its URI.
    if (packagePart != null)
    {
        result = packagePart.Uri;
    }

    return result;
}

private PackagePart GetAnnotationPart(Uri uri)
{
    Package package = PackageStore.GetPackage(documentUri);

    if (package == null)
    {
        return null;
    }

    // Get the FixedDocumentSequence part from the package.
    PackagePart fdsPart = package.GetPart(uri);
// Search through all the document relationships to find the
// annotations relationship part (or null, of there is none).
PackageRelationship annotRel = null;

annotRel
    = fdsPart.GetRelationships().FirstOrDefault<PackageRelationship>(
        pr => pr.RelationshipType == annotRelsType);

PackagePart annotPart;

//If annotations relationship does not exist, create a new
//annotations part, if required, and a relationship for it.
if (annotRel == null)
{
    Uri annotationUri =
        PackUriHelper.CreatePartUri(new Uri("annotations.xml",
                                    UriKind.Relative));

    if (package.PartExists(annotationUri))
    {
        annotPart = package.GetPart(annotationUri);
    }
    else
    {
        //Create a new Annotations part in the document.
        annotPart = package.CreatePart(annotationUri, annotContentType);
    }

        //Create a new relationship that points to the Annotations part.
        fdsPart.CreateRelationship(annotPart.Uri,
                                   TargetMode.Internal,
                                   annotRelsType);
}
else
{
    //If an annotations relationship exists,
    //get the annotations part that it references.

    //Get the Annotations part specified by the relationship.
    annotPart = package.GetPart(annotRel.TargetUri);

    if (annotPart == null)
    {
        //The annotations part pointed to by the annotation
        //relationship URI is not present. Handle as required.
        return null;
    }
}

       return annotPart;
      }
   }
}

Print a Document's Annotations

Problem

You need to print a document including all of its annotations, as displayed in a document viewer.

Solution

Use a System.Windows.Annotations.AnnotationDocumentPaginator to decorate a document with its associated annotations.

How It Works

Printing a document with its annotations is similar to printing a document on its own; just a few extra steps are required to include any annotations in the printout. The document's annotations, if present, are added to the document being printed by using an AnnotationDocumentPaginator. This class's constructor takes two arguments, a System.Windows. Documents.DocumentPaginator (that is, the document paginator for the document you are printing) and a System.Windows.Annotations.AnnotationStore (or an annotation storage stream). The AnnotationDocumentPaginator wraps the source DocumentPaginator and adds the annotations found in the AnnotationStore to the supplied document. This paginator is then used in the same way as writing a normal paginator to a print queue or disk. In this instance, you will pass the annotation paginator as a parameter to the Write method of a System.Windows.Xps.XpsDocumentWriter, created against a System.Printing.PrintQueue (see recipe 7-6).

When printing in this way, a copy of the document's paginator must be used in the AnnotationDocumentPaginator's constructor; otherwise, you will have all sorts of odd behavior happening. This is due to the document becoming corrupt during the printing process and can manifest itself in many ways.

Another gotcha happens when printing annotations asynchronously. If a reference to the annotation store object is used and the annotations in the UI are changed while the document is printing, those changes may well make their way onto the printed document. To combat this, there is a constructor on the AnnotationDocumentPaginator, which accepts a System.IO.Stream object for the annotation store. Use this and pass in a copy of your annotation store Stream object.

Finally, when a document is printed using an AnnotationDocumentPaginator, the annotations will be printed exactly as they appear on the document in the document viewer. Sticky notes that obscure any text will obscure the same text in the final document.

Note

You will need to add references to the System.Printing and ReachFramework assemblies in your project for this example.

The Code

<Window
  x:Class="Recipe_07_16.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"
  Height="300"
  Width="300">
    <DockPanel>
      <DocumentViewer x:Name="dvDocumentViewer">
        <DocumentViewer.CommandBindings>
          <CommandBinding
            Command="ApplicationCommands.Print"
            Executed="DocumentViewer_PrintDocument" />
          </DocumentViewer.CommandBindings>
          <DocumentViewer.ContextMenu>
            <ContextMenu>
              <MenuItem
                Header="Add Comment..."
                Click="DocumentViewer_AddComment" />
            </ContextMenu>
          </DocumentViewer.ContextMenu>
        </DocumentViewer>
      </DockPanel>
    </Window>

The following code defines the content of Window1.xaml.cs:

using System;
using System.IO;
using System.Printing;
using System.Reflection;
using System.Windows;
using System.Windows.Annotations;
using System.Windows.Annotations.Storage;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Xps;
using System.Windows.Xps.Packaging;
namespace Recipe_07_16
{
     /// <summary>
     /// Interaction logic for Window1.xaml
     /// </summary>
     public partial class Window1 : Window
     {
         //Fields to handle our Annotation specifics.
         private AnnotationService fixedAnnotationService;
         private AnnotationStore fixedAnntationStore;
         private Stream fixedAnnotationBuffer;

         private XpsDocument xpsDocument;
         private FixedDocumentSequence fixedDocumentSequence;

         public Window1()
         {
             InitializeComponent();
             //Load in our fixed document.
             LoadFixedDocument();
             //Fire up the annotation service...
             StartFixedDocumentAnnotations();
         }

         private void DocumentViewer_AddComment(object sender, RoutedEventArgs e)
         {
             //Get the current user's name as the author
             string userName = System.Environment.UserName;

             //The AnnotationHelper.CreateTextStickyNoteForSelection method
             //will throw an exception if no text is selected.
             try
             {
                 AnnotationHelper.CreateTextStickyNoteForSelection(
                     fixedAnnotationService, userName);
             }
             catch (InvalidOperationException)
             {
                 MessageBox.Show("Please select some text to annotate.");
             }
         }

         //An XPS document is loaded and displayed in the document viewer,
         //ready for annotating.
         private void LoadFixedDocument()
{
    string documentPath =
        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
        + "\SampleDocument\FixedDocument.xps";

    //Create a URI for the file path.
    Uri documentUri = new Uri(documentPath, UriKind.Absolute);

    xpsDocument = null;

    try
    {
        //Attempts to open the specified XPS document with
        //read and write permission.
        xpsDocument = new XpsDocument(documentPath,
            FileAccess.ReadWrite);
    }
    catch (Exception)
    {
        //You may want to handle any errors that occur during
        //the loading of the XPS document. For example an
        //UnauthorizedAccessException will be thrown if the
        //file is marked as read-only.
    }

    //If the document is null, it's not a valid XPS document.
    if (xpsDocument == null)
        return; //Handle as required.

    //Get the FixedDocumentSequence of the loaded document.
    fixedDocumentSequence
        = xpsDocument.GetFixedDocumentSequence();

    //If the document's FixedDocumentSequence is not found,
    //the document is corrupt.
    if (fixedDocumentSequence == null)
        return; //Handle as required.

    //Load the document's FixedDocumentSequence into
    //the DocumentViewer control.
    dvDocumentViewer.Document = fixedDocumentSequence;
}

private void StartFixedDocumentAnnotations()
{
    //If there is no AnnotationService yet, create one.
    if (fixedAnnotationService == null)
fixedAnnotationService
            = new AnnotationService(dvDocumentViewer);

    //If the AnnotationService is currently enabled, disable it
    //because you'll need to reenable it with a new store object.
    if (fixedAnnotationService.IsEnabled)
        fixedAnnotationService.Disable();

    //Open a memory stream for storing annotations.
    fixedAnnotationBuffer = new MemoryStream();

    //Create a new AnnotationStore using the above stream.
    fixedAnntationStore = new XmlStreamStore(fixedAnnotationBuffer);

    //Enable the AnnotationService using the new store object.
    fixedAnnotationService.Enable(fixedAnntationStore);
}

//Present the user with a PrintDialog, allowing them to
//select and configure a printer.
public PrintQueue ShowPrintDialog()
{
    PrintDialog printDialog = new PrintDialog();

    if (printDialog.ShowDialog() == true)
        return printDialog.PrintQueue;

    return null;
}

//Handles the click of the print button in the document
//viewer, overriding the default behavior.
private void DocumentViewer_PrintDocument(object sender, RoutedEventArgs e)
{
    //Get a print queue
    PrintQueue printQueue = ShowPrintDialog();

    if (printQueue == null)
        return;

    try
    {

        //Create a new XPS writer using the chosen print queue.
        XpsDocumentWriter writer
            = PrintQueue.CreateXpsDocumentWriter(printQueue);
//We need to use a copy of the document's fixed document
              //sequence when creating the AnnotationDocumentPaginator.
              FixedDocumentSequence fds
                  = xpsDocument.GetFixedDocumentSequence();

              //You now need to create a document paginator for any
              //annotations in the document.
              AnnotationDocumentPaginator adp =
                  new AnnotationDocumentPaginator(fds.DocumentPaginator,
                      fixedAnnotationService.Store);

              //Write out the document, with annotations using the annotation
              //document paginator.
              writer.Write(adp);
          }
          catch (Exception ex)
          {
              MessageBox.Show(ex.Message);
          }
       }

    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset