(X) Hide this
    • Login
    • Join
      • Say No Bots Generate New Image
        By clicking 'Register' you accept the terms of use .

Advanced printing in Silverlight

(9 votes)
Jonathan van de Veen
>
Jonathan van de Veen
Joined Aug 27, 2009
Articles:   4
Comments:   12
More Articles
22 comments   /   posted on May 18, 2010
Categories:   Line-of-Business , General

This article is compatible with the latest version of Silverlight.


Introduction

Some time ago Corey Schuman published the article A look at the Printing API in Silverlight right here on SilverlightShow.net. The article deals with the basics of printing in Silverlight, using a WYSIWYG approach. In this article we want to look into a more advanced scenario of printing, where WYSIWYG is not the best approach. We’ll take a look at how to print text in such a way that it will fit on the page and we’ll look into printing more than one page.

Before we start, a screenprint of what we’re building:

PrintDemoScreen

And if you want to have a look at the result, you can do so here. You can download the project here.

The concept

Before we dive into the code, let’s take a look at how we want our printing demo to work. First of all we want to print multiple pages with a header and a footer on each page. We also want our text to always fit on the page, instead of the text being formatted exactly like on screen. And finally we want our text to span multiple pages as needed.

In order to meet these requirements we need a separate user control that will be send to the printing API. The user control needs to provide a format containing the header, body and footer. In order to achieve a layout like that I chose to use a Grid with three rows. One thing to be aware of is that the Grid will automatically take up all the space on the page, which is exactly what we want. The reason it does this is that it is never displayed on screen, so it’s parent will actually be the page itself. Therefore the page will dictate the maximum allowed space for the grid.

Once we have that, we need to make sure that we only put just enough text on a page to fill it, not more and not less. To do this, we can use the layout system in Silverlight to make sure our content fits on the page. We’ll combine it with a trial-and-error approach to find the exact content to fit on the page. You can use the same concept for printing data on a page as well.

Step 1: Laying out the pages

Before we can print anything, we need to create a UserControl to hold the layout. I created a UserControl called PrintPage which does exactly that. The XAML code is kept simple to demonstrate the principles:

 <Grid x:Name="documentRoot">
     <Grid.RowDefinitions>
         <RowDefinition Height="25"/>
         <RowDefinition />
         <RowDefinition Height="25"/>
     </Grid.RowDefinitions>
     
     <TextBlock x:Name="headerTextBlock" HorizontalAlignment="Center" />
     
     <TextBlock x:Name="bodyTextBlock" Grid.Row="1" TextWrapping="Wrap" />
     
     <TextBlock x:Name="footerTextBlock" HorizontalAlignment="Center" Grid.Row="2"/>
 </Grid>

As you can see, there is a Grid with three rows of which the middle row will size based on it’s content. The middle row contains the TextBlock that will contain our body text. The top and bottom rows contain the TextBlocks for the header and footer.

There is also some simple code to allow us to fill the page:

 public partial class PrintPage : UserControl
 {
     public PrintPage()
     {
         InitializeComponent();
     }
  
     public void SetHeaderAndFooterText(string header, string footer)
     {
         headerTextBlock.Text = header;
         footerTextBlock.Text = footer;
     }
   
     public void AddLine(string line)
     {
         bodyTextBlock.Inlines.Add(line);
         bodyTextBlock.Inlines.Add(new LineBreak());
     }
   
     public void RemoveLastLine()
     {
         for (int index = 0; index < 2; index++)
         {
             bodyTextBlock.Inlines.RemoveAt(bodyTextBlock.Inlines.Count - 1);
         }
     }
 }

So we have a method to set both the header and footer for the page. More important are the methods to add and remove lines from the body. The code is pretty simple, but we’ll look into how they are used later on.

Step 2: Getting ready to print

Now that we have a PrintPage control, we are ready to use it for printing. I’ll spare you the details on building a test application around it and skip right to the good stuff. In the article by Corey Schuman you’ve already had an introduction to the PrintDocument class and the printing API. In order for us to print a document on multiple pages we need a more advanced use of the same components:

 _lineIndex = 0;
 _documentBodyLines = new List<string>();
   
 string[] lines = bodyTextBox.Text.Split(new char[] { '\r' }, StringSplitOptions.None);
 _documentBodyLines.AddRange(lines);
  
 PrintDocument printDocument = new PrintDocument();
  
 printDocument.BeginPrint += new EventHandler<BeginPrintEventArgs>(printDocument_BeginPrint);
 printDocument.EndPrint += new EventHandler<EndPrintEventArgs>(printDocument_EndPrint);
 printDocument.PrintPage += new EventHandler<PrintPageEventArgs>(printDocument_PrintPage);
 printDocument.Print("SLPrintDemo document");

First we reset the _lineIndex field, which we will be needing during printing. Next we take the complete document text and prepare it for printing, by splitting it into separate lines. Next we setup the PrintDocument class we will be using for print. Besides attaching to the PrintPage event, we also attach handlers to BeginPrint and EndPrint so we can give some feedback to the user on when we are done printing. And finally we call Print to bring up the print dialog.

The reason I’ve not used an inline anonymous method is that the code in the PrintPage event handler is more then just a couple of lines. Putting the code inline here would make it unreadable.

Step 3: Printing a page

Now that we have setup our PrintDocument class we can focus on printing a single page. Note that we can’t tell what page we are printing from the PrintPage event handler. We can only tell what page we are printing from our own state. Let’s take a look at the code:

 void printDocument_PrintPage(object sender, PrintPageEventArgs e)
 {
     PrintPage page = new PrintPage();
     page.SetHeaderAndFooterText(headerTextBox.Text, footerTextBox.Text);
  
     int numberOfLinesAdded = 0;
     while (_lineIndex < _documentBodyLines.Count)
     {
         page.AddLine(_documentBodyLines[_lineIndex]);
         page.Measure(new Size(e.PrintableArea.Width, double.PositiveInfinity));
         if (page.DesiredSize.Height > e.PrintableArea.Height 
             && numberOfLinesAdded > 1)
         {
             page.RemoveLastLine();
             e.HasMorePages = true;
             break;
         }
         _lineIndex++;
         numberOfLinesAdded++;
     }
   
     e.PageVisual = page;
 }

We obviously start by creating a PrintPage control to represent our page in the printing API and then we set the header and footer by calling the SetHeaderAndFooterText method. Next we initialize a variable called numberOfLinesAdded which will hold the number of lines we have added to the page. Then we start looping through the lines of the document, which we initialized before we started printing.

Inside the loop we start adding lines to the current page. Then we call the Measure method on the page provided by Silverlight. This will update the DesiredSize property of a UserControl. In our application it will adjust it according to the amount of text included in the body. Then we check if the height our page would be can fit into the PrintibleArea as it is provided by the printing API. If it doesn’t fit and we did add at least two lines, we will then remove the last line. Then we will tell the printing API we need to print another page by setting the HasMorePages property of the event arguments to true and break out of the loop. If we have not exceeded the available height, we will then move on the the next line.

As we exit the loop we then set the PageVisual property to represent our PrintPage control, which prints the page.

Conclusion

So here is what we’ve seen in the article:

  • Making sure your content fits on the page is easy if you use the Silverlight layout system
  • If you want your content to fit on the page, don’t use on screen controls
  • The HasMorePages property of the PrintDocument class is your friend in these scenario’s

Subscribe

Comments

  • -_-

    RE: Advanced printing in Silverlight 4


    posted by Marnei on May 19, 2010 16:54
    Wow!
  • -_-

    RE: Advanced printing in Silverlight 4


    posted by Chris Bordeman on Jun 09, 2010 07:34
    Really nice, just what I was looking for.  Thanks!
  • -_-

    RE: Advanced printing in Silverlight 4


    posted by Balaji on Jun 21, 2010 07:43

    hi jonathon

    Can it be possible to impolement this logic for the xaml with

    panel bar inside the Grid with many rows and Columns.

    Can you pls get back

    Thanks

    Regards

    N.Balaji

  • -_-

    RE: Advanced printing in Silverlight 4


    posted by Vishal on Jul 10, 2010 19:09
    Excellent example. Just what I was looking for, creating a custom printing template and use it to print multiple pages. Thank you very much
  • -_-

    RE: Advanced printing in Silverlight 4


    posted by Ethan on Jul 12, 2010 22:17
    Is there a way to set page layout t landscape?
  • mrjvdveen

    RE: Advanced printing in Silverlight 4


    posted by mrjvdveen on Jul 13, 2010 09:40

    @Ethan: Wow, you must be eager for an answer :-).

    Unfortunately there is no way to set the page layout (yet?). A security consideration, perhaps?

    @Balaji: I'm not sure what you mean, but you can adjust this approach to fit most needs. It's simply a way of preventing a page overload and making sure everything is printed.

  • -_-

    RE: Advanced printing in Silverlight 4


    posted by SC Vinod on Jul 21, 2010 08:15

    Hi Jonathan,

                        I need to print a very long Silverlight DataGrid....When I use PrintDocument Only half of the DataGrid is getting printed... Can you please suggest me a solution....I also need to add a header to my datagrid getting printed. Thanks in advance.

  • mrjvdveen

    RE: Advanced printing in Silverlight 4


    posted by mrjvdveen on Jul 21, 2010 09:31

    @SC Vinod: The approach I demonstrated above is exactly what you need. Just replace the bodyTextBlock in the PrintPage for a DataGrid and remove the footerTextBlock. Then instead of adding text to the bodyTextBlock you should databind a new datasource to the DataGrid and keep adding records until the PrintPage no longer fits on paper and then remove the last record.

    Keep track of what record you added last and you can continue on the next page. It's really the same algorithm, just with slightly different ingredients.

    HTH. If you have questions, just let us know.

  • -_-

    RE: Advanced printing in Silverlight 4


    posted by SC Vinod on Jul 22, 2010 09:20

    Hi Jonathan,

                      Thanks a ton for your reply. I got some ideas on how to print my datagrid. Now the issue that I'm facing is my DataGrid is too wide in width so only 60% of the width of my DataGrid is getting printed. If the height is the problem while printing we can remove the last record and set HasMorePages = true, but what can we do if the width of the page is greater than the width of the PrintableArea? 

  • mrjvdveen

    RE: Advanced printing in Silverlight 4


    posted by mrjvdveen on Jul 22, 2010 09:26

    Hi SC Vinod,

    You could use the same approach to also account for the width of the page. It's more complex. You would first have to make sure you only display the columns you can fit on the page and then adjust for the amount of records. Then on the second page, you would print the same records, but you would have to print different columns. Then on the third page (assuming it would fit on two pages) you would print the second set of records showing the first batch of columns, etc..

    It's the same approach, only more complex, because you have the same thing twice, but now impacting each other.

    HTH.

  • -_-

    RE: Advanced printing in Silverlight 4


    posted by Jacek Jura on Nov 30, 2010 09:39

    My solution of fit-to-page Silverlight printing:

    Silverlight printing: fit-to-page.

  • -_-

    RE: Advanced printing in Silverlight


    posted by Fillipe on May 31, 2011 14:10

    Hi Jonatthan! Great article! Many thanks for sharing!

    Chrome OS

  • Paran

    Re: Advanced printing in Silverlight


    posted by Paran on Jul 22, 2011 17:43

    Hi Jonathan,

     Is it possible to implement the 'Page Setup' concept like margin alignment before sending it for printing?

  • dtm

    Re: Advanced printing in Silverlight


    posted by dtm on Feb 15, 2012 06:22

    Hi Jonatthan

    I too am trying to print a large data grid with many, many hundreds of rows. I'm trying to follow your suggestion to SC Vinod above but with no luck. I can get the grid fine but as I add rows to my new ItemSouce on PrintPage control, it simply grows and the DataGrid scrolls, so the printed result is never more than a single page.

     

    I've resorted to manually building rows by assembling TextBocks as fields and adding to a Stackpanel, one StackPanel per row. That works but I feel like there should be an easier way to simply print the DataGrid, but only with rows that fit on one page.

     

    Any additional code snippets or advice would be greatly appreciated.

     

    Thanks for the article

  • JustinHalls

    Re: Advanced printing in Silverlight


    posted by JustinHalls on Feb 23, 2012 13:42

    Wonderful!  I have been trying to do a multi-page print from a Lightswitch Custom Control which databinds the (too large for one page) data into a datagrid.  In the Loaded event handler I copied all the data from dataGrid.ItemsSource into a List<object> and then in the printPage() function I create a new List<object> into which I copy a subset of the total data.  Assign this subset List to be the dataGrid ItemsSource and call this.UpdateLayout() before setting pageVisual to the LayoutRoot.

    I do have to use the original CustomControl throughout. if I creata a new instance to laod with the datasubset then I get an exception saying that the element is already the child of another element (even though its local created afresh for each page).

    Multi-page printing for lightswitch is major breakthrough, I havent seen it done anywhere before.

    Many thanks for you article.

     

  • JustinHalls

    Re: Advanced printing in Silverlight


    posted by JustinHalls on Feb 23, 2012 13:59

    Wonderful!  I have been trying to do a multi-page print from a Lightswitch Custom Control which databinds the (too large for one page) data into a datagrid.  In the Loaded event handler I copied all the data from dataGrid.ItemsSource into a List<object> and then in the printPage() function I create a new List<object> into which I copy a subset of the total data.  Assign this subset List to be the dataGrid ItemsSource and call this.UpdateLayout() before setting pageVisual to the LayoutRoot.

    I do have to use the original CustomControl throughout. if I creata a new instance to laod with the datasubset then I get an exception saying that the element is already the child of another element (even though its local created afresh for each page).

    Multi-page printing for lightswitch is major breakthrough, I havent seen it done anywhere before.

    Many thanks for you article.

     

  • JustinHalls

    Re: Advanced printing in Silverlight


    posted by JustinHalls on Feb 23, 2012 15:03

    Wonderful!  I have been trying to do a multi-page print from a Lightswitch Custom Control which databinds the (too large for one page) data into a datagrid.  In the Loaded event handler I copied all the data from dataGrid.ItemsSource into a List<object> and then in the printPage() function I create a new List<object> into which I copy a subset of the total data.  Assign this subset List to be the dataGrid ItemsSource and call this.UpdateLayout() before setting pageVisual to the LayoutRoot.

    I do have to use the original CustomControl throughout. if I creata a new instance to laod with the datasubset then I get an exception saying that the element is already the child of another element (even though its local created afresh for each page).

    Multi-page printing for lightswitch is major breakthrough, I havent seen it done anywhere before.

    Many thanks for you article.

     

  • dtm

    Re: Advanced printing in Silverlight


    posted by dtm on Feb 23, 2012 18:45

    Justin

    How do you know how many items to put in your subset so that the new datagrid is not going to scroll? I cant figure out how to determine how many rows will fit in a data grid on a single page. Any advice would be appreciated.

    Thanks

    dtm

  • JustinHalls

    Re: Advanced printing in Silverlight


    posted by JustinHalls on Feb 24, 2012 10:06

    dtm

    I cheated in my application by pre-guesssing the number of rows to use that would work for an A4 paper.  Otherwise you need to do it by calculation - you know the row height of the datagrid because you designed it that way, and you can find the datagrid.ActualHeight and calculate the number of rows that will fit.  I havent tried it but it might also be possible to check to see if the vertical slider is visible/movable but you might need to do the UpdateLayout first so you wouldnt want to do that for each row, just as a final check that all the rows were visible.

    The other comment I would make is to try to keep the proportions of the custom control the same as those of the paper so that if you stretch the control to fit the page it doesnt get distorted.

    Hope this helps

    Justin

  • dtm

    Re: Advanced printing in Silverlight


    posted by dtm on Feb 25, 2012 06:03

    Thanks Justin!

    I'll give that a try

  • bpraveenraju

    Re: Advanced printing in Silverlight


    posted by bpraveenraju on Sep 06, 2012 13:46
    I have a very big xaml page , which consists of many items(data grids, canvas, item controls..........) .I want to print entire page and the number of pages will change randomly .How to achieve this?Is it possible?(I know printing data if we have only one grid or normal text).
  • MHALottering

    Re: Advanced printing in Silverlight


    posted by MHALottering on Dec 17, 2012 12:30
    May I add a small suggestion?

    If you battle to get the landscape printing working properly because it cuts off as if still printing portrait:


    Use a `canvas` container control, and **rotate** the control **inside of the canvas**.

    After many hours of pure frustration, I finally decided to put the thing inside of a canvas. Voila!! No more cut off.

    Have a look at the following SMALL test application to demonstrate. It is just a page with print button.

    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;
    using System.Windows.Printing;
    using System.Windows.Shapes;
     
    namespace SilverlightApplication1 {
     
        public partial class MainPage : UserControl {
     
            public MainPage() {
                InitializeComponent();
            }
     
            private void Button_Click_1(object sender, RoutedEventArgs e) {
                PrintDocument PD = new PrintDocument();
                PD.PrintPage += PD_PrintPage;
                PD.Print("Print Test");
            }
     
            void PD_PrintPage(object sender, PrintPageEventArgs e) {
     
                Canvas OuterCanvas = new Canvas();
     
                /* a container for everything that will print */
                Border OuterBorder = new Border() {
                    BorderThickness = new Thickness(3),
                    BorderBrush = new SolidColorBrush(Colors.Red),
                    Margin = new Thickness(10)
                };
     
                double Width = e.PrintableArea.Width - OuterBorder.Margin.Left - OuterBorder.Margin.Right;
                double Height = e.PrintableArea.Height - OuterBorder.Margin.Top - OuterBorder.Margin.Bottom;
     
                /* NOTE: We're trying to force landscape, so swop the width and height */
                OuterBorder.Width = Height;
                OuterBorder.Height = Width;
                 
                /* on portrait, this line goes down (leave the printer settings, we're trying to force landscape) */
                Line Line = new Line() {
                    X1 = OuterBorder.Width / 2,
                    Y1 = 0,
                    X2 = OuterBorder.Width / 2,
                    Y2 = OuterBorder.Height,
                    Stroke = new SolidColorBrush(Colors.Blue),
                    StrokeThickness = 3
                };
     
                OuterBorder.Child = Line;
     
                OuterCanvas.Children.Add(OuterBorder);
     
                /* rotate 90 degrees, and move into place */
                var transformGroup = new TransformGroup();
                transformGroup.Children.Add(new RotateTransform() { Angle = 90 });
                transformGroup.Children.Add(new TranslateTransform() { X = e.PrintableArea.Width });
                OuterBorder.RenderTransform = transformGroup;
     
                e.PageVisual = OuterCanvas;
     
                e.HasMorePages = false;
            }
        }
    }

    If you don't put the border inside of a canvas, it causes the page to appear cut off as if still printing portrait.

    Regards,
    Martin

Add Comment

Login to comment:
  *      *