Printing with the UWP Community Toolkit

In this article we describe some advanced printing scenarios on top of the PrintHelper from the UWP Community Toolkit. This helper is specialized in interacting with the built-in print dialogs, and in printing one UI element on one page. The latter sounds pretty limited, but in the next couple of paragraphs we’ll show things like

  • wrapping the helper in an MVVM service,
  • adding a header and/or a footer to the print pages,
  • adding page numbers to the print pages,
  • printing a list of items, each on its own page, and
  • printing a more complex report.

That should suffice for most UWP apps that have printing on their list of requirements – basically all apps that don’t need dynamic page breaks.

As usual, there’s a sample app on GitHub. It comes with several pages with a large ‘Print’ button:

 ContentPage

Here are the main classes in that sample app:

  • Content pages: the app’s pages that provide content and layout of the information to be printed,
  • PrintService: a class that provides printing-as-a-service in an MVVM architecture,
  • PrintPage: a helper class that prepares the print content (a FrameworkElement) with header, footer, and page numbering, and
  • PrintHelper: the toolkit’s print helper that knows how to print a FrameworkElement on a single page.

Here’s a class diagram of the core print engine:

ClassDiagram1

Creating an MVVM PrintService

The Print Service is the central place to which the different content pages send the elements to be printed:

  • one header, one footer, and one place to put the page for the entire job, and
  • one main content per individual print page.

The service registers the handlers for the system print dialog events (to deal with success, failure, or cancellation), manipulates the content through the smart PrintPage (see further), and hands the result over to the toolkit PrintHelper.

Printing XAML –with or without the PrintHelper- requires the XAML elements to be rendered on an invisible panel in your Visual Tree before they’re moved to separate print preview pages in the system print dialog. Your app needs to provide that invisible panel.

In an MVVM environment the so-called Shell (the page that hosts the navigation panel) is an ideal place to host this panel. We’ll call it the PrintingContainer:

    <Grid>
        <!-- Required for the PrintService -->
        <Grid x:Name="PrintingContainer"
              Opacity="0" />

        <!-- SplitView -->
        <SplitView x:Name="SplitView"
        <!-- ... -->
        </SplitView>

        <!-- Hamburger Button -->
        <Button x:Name="HamburgerButton">
        <!-- ... -->
        </Button>
    </Grid>

When the app starts, it provides this field to the central print service:

// Initialize Printing Service.
PrintService.PrintingContainer = PrintingContainer;

The PrintService class takes a Header, Footer and PageNumbering property. For the rest it host all the code for working with the UWP Community Toolkit’s PrintHelper class that you find in the official documentation. In the Print method you see that the service creates a PrintPage (see further) for each page to be sent to the PrintHelper. The print service protects the content pages from all that knowledge.

Here’s the whole class:

public class PrintService
{
    private static Panel _printingContainer;

    private PrintHelper _printHelper;
    private List<FrameworkElement> _content = new List<FrameworkElement>();
    private FrameworkElement _header;
    private FrameworkElement _footer;
    private PageNumbering _pageNumbering = PageNumbering.None;

    public PrintService()
    {}

    public static Panel PrintingContainer
    {
        set { _printingContainer = value; }
    }

    public FrameworkElement Header
    {
        set { _header = value; }
    }

    public FrameworkElement Footer
    {
        set { _footer = value; }
    }

    public PageNumbering PageNumbering
    {
        set { _pageNumbering = value; }
    }

    public void AddPrintContent(FrameworkElement content)
    {
        _content.Add(content);
    }

    public void Print()
    {
        _printHelper = new PrintHelper(_printingContainer);

        PrintPage.StartPageNumber = 1;
        foreach (var content in _content)
        {
            var page = new PrintPage(content, _header, _footer, _pageNumbering);
            _printHelper.AddFrameworkElementToPrint(page);
        }

        _printHelper.OnPrintFailed += printHelper_OnPrintFailed;
        _printHelper.OnPrintSucceeded += printHelper_OnPrintSucceeded;
        _printHelper.OnPrintCanceled += printHelper_OnPrintCanceled;

        _printHelper.ShowPrintUIAsync("Print Sample");
    }

    private void printHelper_OnPrintCanceled()
    {
        ReleasePrintHelper();
    }

    private void printHelper_OnPrintSucceeded()
    {
        Toast.ShowInfo("Printed.");
        ReleasePrintHelper();
    }

    private void printHelper_OnPrintFailed()
    {
        Toast.ShowError("Sorry, printing failed.");
        ReleasePrintHelper();
    }

    private void ReleasePrintHelper()
    {
        _printHelper.Dispose();
    }
}

Here’s how a successful print notification looks like, in the sample app:

Notification

Adding headers and footers

The main job of the PrintPage class in the sample app is to transform all print content for a single page (main content, header, footer, page numbering) into a single FrameworkElement, since the toolkit’s PrintHelper only accepts single FrameworkElement instances to be printed.

The PrintPage takes all print content and lays it out in a 3-row Grid (header – main beef – content):

Grid _printArea;

public PrintPage(FrameworkElement content, FrameworkElement header = null, FrameworkElement footer = null, PageNumbering pageNumbering = PageNumbering.None)
{
    _printArea = new Grid();
    _printArea.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
    _printArea.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });
    _printArea.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
    _printArea.Children.Add(new Canvas() { Width = 10000 }); // Horizontally stretches the Grid.

    Content = _printArea;

    AddContent(content);
    Header = header;
    Footer = footer;
}

When the Header is assigned, it is immediately cloned -because it will possibly be used on multiple page instances- and placed in the top row of the grid:

public FrameworkElement Header
{
    set
    {
        if (value != null)
        {
            var header = value.DeepClone();
            Grid.SetRow(header, 0);
            _printArea.Children.Add(header);
        }
    }
}

The header and footer are cloned because they will appear on each page in the print job and a UI element can only have one parent. I would have preferred to use a XamlWriter to transform the element into a XAML string, and then a XamlReader to instantiate a clone from this XAML. Unfortunately XamlWriter does not exist in UWP, so we have to rely on reflection.

The DeepClone method that we’re using, is all over the internet – it’s impossible to find the original source. It checks the origin type, creates a new instance from it, and then recursively clones its properties (except the Name) and children.

Here’s how the first content page in the sample app talks to the print service – instantiate, assign header and footer, add print pages, call Print method:

var service = new PrintService();

// Define Header, Footer, and Page Numbering.
service.Header = new TextBlock() { Text = "Header", Margin = new Thickness(0, 0, 0, 20) };
service.Footer = new TextBlock() { Text = "Footer", Margin = new Thickness(0, 20, 0, 0) };
service.PageNumbering = PageNumbering.TopRight;

// Add three pages.
service.AddPrintContent(new TextBlock() { Text = "Hello World!" });
service.AddPrintContent(new Image() { Source = new BitmapImage(new Uri("ms-appx:///Assets/world.png")) });
service.AddPrintContent(new TextBlock() { Text = "Goodbye World!" });

service.Print();

Adding page numbering

Once we know how to add a header and a footer to the print pages, adding page numbers is trivial. Let’s start with defining an enumeration for page number positions:

public enum PageNumbering
{
    None,
    TopLeft,
    TopMiddle,
    TopRight,
    BottomLeft,
    BottomMidle,
    BottomRight
}

Each time we add a new print page to the print job with AddContent(), we check if a page number is required. If so, we increment the value and place a TextBlock in the appropriate place (be careful: the page number will appear in row 0 or 2 of the print page grid, so it may clash with header or footer – it’s up to you to avoid this):

public void AddContent(FrameworkElement content)
{
    Grid.SetRow(content, 1);
    _printArea.Children.Add(content);

    if (PageNumbering != PageNumbering.None)
    {
        _pageNumber += 1;
        var pageNumberText = new TextBlock() { Text = _pageNumber.ToString() };

        switch (PageNumbering)
        {
            case PageNumbering.None:
                break;
            case PageNumbering.TopLeft:
                Grid.SetRow(pageNumberText, 0);
                pageNumberText.Margin = new Thickness(0, 0, 0, 20);
                _printArea.Children.Add(pageNumberText);
                break;
            case PageNumbering.TopMiddle:
                Grid.SetRow(pageNumberText, 0);
                pageNumberText.Margin = new Thickness(0, 0, 0, 20);
                pageNumberText.HorizontalAlignment = HorizontalAlignment.Stretch;
                pageNumberText.HorizontalTextAlignment = TextAlignment.Center;
                _printArea.Children.Add(pageNumberText);
                break;
            case PageNumbering.TopRight:
                Grid.SetRow(pageNumberText, 0);
                Grid.SetColumn(pageNumberText, 1);
                pageNumberText.Margin = new Thickness(0, 0, 0, 20);
                pageNumberText.HorizontalAlignment = HorizontalAlignment.Stretch;
                pageNumberText.HorizontalTextAlignment = TextAlignment.Right;
                _printArea.Children.Add(pageNumberText);

                break;
            case PageNumbering.BottomLeft:
                Grid.SetRow(pageNumberText, 2);
                pageNumberText.Margin = new Thickness(0, 20, 0, 0);
                _printArea.Children.Add(pageNumberText);
                break;
            case PageNumbering.BottomMidle:
                Grid.SetRow(pageNumberText, 2);
                pageNumberText.Margin = new Thickness(0, 20, 0, 0);
                pageNumberText.HorizontalAlignment = HorizontalAlignment.Stretch;
                pageNumberText.HorizontalTextAlignment = TextAlignment.Center;
                _printArea.Children.Add(pageNumberText);
                break;
            case PageNumbering.BottomRight:
                Grid.SetRow(pageNumberText, 2);
                pageNumberText.Margin = new Thickness(0, 20, 0, 0);
                _printArea.Children.Add(pageNumberText);
                break;
            default:
                break;
        }
    }
}

Here’s how all of this looks like at runtime in the sample app:

HeaderFooterPageNumbers

Printing a list on separate pages

Maybe your app needs to print a collection of items, typically displayed through some kind of ItemsControl. The UWP Community Toolkit’s PrintHelper can’t do dynamic page breaks, so we’ll start each item on a new page.

The print content instances are created from a DataTemplate, like this:

<Page.Resources>
    <DataTemplate x:Name="MoonTemplate">
        <StackPanel>
            <TextBlock Text="{Binding Name}"
                        FontSize="48"
                        Margin="60" />
            <Image Source="{Binding ImagePath}"
                    Margin="120 0" />
        </StackPanel>
    </DataTemplate>
</Page.Resources>

To print the collection, just iterate through it and create a ContentControl for each item, load the data template in its ContentTemplate, assign the DataContext to update all bindings, and add the new content to the print job:

var service = new PrintService();
service.Header = new TextBlock { Text = "List of items with page break" };
service.PageNumbering = PageNumbering.BottomMidle;

foreach (var moon in Moons)
{
    // The secret is to NOT use an ItemsControl.
    var cont = new ContentControl();
    cont.ContentTemplate = Resources["MoonTemplate"] as DataTemplate;
    cont.DataContext = moon;
    service.AddPrintContent(cont);
}

service.Print();

Here’s how this looks like in the sample app:

ListItems

Printing a report

Maybe your app has a more or less complex dashboard page, with different texts and images, and controls like gauges and diagrams. That page may have a background image, light text on a dark background, and/or a horizontal layout. So it’s probably not a very good idea to emulate a screenshot and send that page as-is to a printer. For printed reports we expect a white background, probably no background images, and a vertical (portrait) layout. But we still want to reuse as much as possible XAML elements in both ‘pages’. So we’re going to rely on data templates again.

Some data templates can be very simple, like this one that just displays an image:

<DataTemplate x:Name="ImageTemplate">
    <Image Source="{Binding ImagePath}"
            Stretch="Uniform"
            Margin="0" />
</DataTemplate>

But they can also be a bit more complex, like this grid that hosts a list of characteristics:

<DataTemplate x:Name="CharacteristicsTemplate">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <TextBlock Text="Planet"
                    FontWeight="SemiBold"
                    HorizontalAlignment="Right"
                    Margin="0 0 10 10" />
        <TextBlock Text="{Binding Planet}"
                    Margin="10 0 0 10"
                    Grid.Column="1" />
        <TextBlock Text="Mass"
                    FontWeight="SemiBold"
                    HorizontalAlignment="Right"
                    Margin="0 0 10 10"
                    Grid.Row="1" />
        <TextBlock Text="{Binding Mass}"
                    Margin="10 0 0 10"
                    Grid.Row="1"
                    Grid.Column="1" />
        <TextBlock Text="Albedo"
                    FontWeight="SemiBold"
                    Margin="0 0 10 10"
                    HorizontalAlignment="Right"
                    Grid.Row="2" />
        <TextBlock Text="{Binding Albedo}"
                    Margin="10 0 0 10"
                    Grid.Row="2"
                    Grid.Column="1" />
        <TextBlock Text="Orbital Eccentricity"
                    FontWeight="SemiBold"
                    HorizontalAlignment="Right"
                    Margin="0 0 10 10"
                    Grid.Row="3" />
        <TextBlock Text="{Binding OrbitalEccentricity}"
                    Margin="10 0 0 10"
                    Grid.Row="3"
                    Grid.Column="1" />
    </Grid>
</DataTemplate>

Here’s the horizontal layout of the dashboard page in the sample app. It gets its details from the different data templates:

<ListView SelectionMode="None"
            ScrollViewer.HorizontalScrollMode="Enabled"
            ScrollViewer.IsHorizontalRailEnabled="True"
            ScrollViewer.HorizontalScrollBarVisibility="Auto"
            Grid.Row="1">
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
    <Grid Height="500"
            Width="500">
        <ContentControl DataContext="{x:Bind SelectedMoon}"
                        ContentTemplate="{StaticResource ImageTemplate}" />
    </Grid>
    <Grid Height="500"
            Width="500">
        <ContentControl DataContext="{x:Bind SelectedMoon}"
                        ContentTemplate="{StaticResource DescriptionTemplate}" />
    </Grid>
    <Grid Height="500"
            Width="250">
        <ContentControl DataContext="{x:Bind SelectedMoon}"
                        ContentTemplate="{StaticResource CharacteristicsTemplate}" />
    </Grid>
    <Grid Height="500"
            Width="500">
        <ContentControl DataContext="{x:Bind Moons}"
                        ContentTemplate="{StaticResource BarChartTemplate}" />
    </Grid>
</ListView>

Here’s the template for the vertical print page. It reuses as much of these data templates as possible, but it hangs these in a container with a different look. The data template for the report is in the page’s resources:

<DataTemplate x:Name="ReportTemplate">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <TextBlock DataContext="{Binding SelectedMoon}"
                    Text="{Binding Name}"
                    FontSize="36"
                    Margin="0 10"
                    Grid.ColumnSpan="2" />
        <ContentControl DataContext="{Binding SelectedMoon}"
                        ContentTemplate="{StaticResource ImageTemplate}"
                        Margin="0 0 10 10"
                        Grid.Row="1" />
        <TextBlock DataContext="{Binding SelectedMoon}"
                    Text="{Binding Description}"
                    TextWrapping="Wrap"
                    Margin="10 0 0 10"
                    Grid.Row="1"
                    Grid.Column="1" />
        <ContentControl DataContext="{Binding SelectedMoon}"
                        ContentTemplate="{StaticResource CharacteristicsTemplate}"
                        HorizontalAlignment="Center"
                        Margin="0 0 10 10"
                        Grid.Row="2" />
        <ContentControl RequestedTheme="Dark"
                        DataContext="{Binding Moons}"
                        ContentTemplate="{StaticResource BarChartTemplate}"
                        HorizontalAlignment="Center"
                        Margin="0 0 0 0"
                        Grid.ColumnSpan="2"
                        Grid.Row="3" />
    </Grid>
</DataTemplate>

I also added an example of reusing a more advanced control – a Telerik BarChart. I had to hard code the color scheme (blue with a black border) for the bars, because by default they became white or transparent in the print page:

<DataTemplate x:Name="BarChartTemplate">
    <telerikChart:RadCartesianChart>
        <telerikChart:RadCartesianChart.VerticalAxis>
            <telerikChart:LinearAxis Minimum="0"
                                        Maximum="1"
                                        Title="Albedo"
                                        FontFamily="Segoe UI" />
        </telerikChart:RadCartesianChart.VerticalAxis>
        <telerikChart:RadCartesianChart.HorizontalAxis>
            <telerikChart:CategoricalAxis />
        </telerikChart:RadCartesianChart.HorizontalAxis>
        <telerikChart:BarSeries ItemsSource="{Binding}">
            <telerikChart:BarSeries.DefaultVisualStyle>
                <Style TargetType="Border">
                    <Setter Property="Background"
                            Value="DodgerBlue" />
                    <Setter Property="BorderBrush"
                            Value="Black" />
                    <Setter Property="BorderThickness"
                            Value="1" />
                </Style>
            </telerikChart:BarSeries.DefaultVisualStyle>
            <telerikChart:BarSeries.CategoryBinding>
                <telerikChart:PropertyNameDataPointBinding PropertyName="Name" />
            </telerikChart:BarSeries.CategoryBinding>
            <telerikChart:BarSeries.ValueBinding>
                <telerikChart:PropertyNameDataPointBinding PropertyName="Albedo" />
            </telerikChart:BarSeries.ValueBinding>
        </telerikChart:BarSeries>
    </telerikChart:RadCartesianChart>
</DataTemplate>

Here are the two pages, in the sample app:

DashboardToReport

In this article we showed some easy tricks to create more complex print jobs on top of the UWP Community Toolkit. If your app needs more complexity than this, then I would suggest to take a look at my dynamic page-break solution or let your app expose its information through PDF or HTML.

The sample app lives here on GitHub.

Enjoy!

Advertisements

2 thoughts on “Printing with the UWP Community Toolkit

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s