I know we’re all living here in the 21st century and we’re supposed to be paperless and everything but I still print quite a lot of stuff out. I’ve got one of those all-in-one printers on my desk here and I still probably send a few sheets to it every day. Often it’s things that I know I have to keep ( e.g. expense form copies ) or that I know I will be stuck without should technology fail me ( e.g. phone numbers and locations of an office or venue I might be driving to ).
So, for me, printing’s important and it’s great to see printing support there for Windows Store apps and here’s a couple of quick references;
Those are the official links. Printing is one of those areas where the WinRT platform has the core support and then the UI technologies (XAML/HTML) handle it differently and I’m only going to do a sketched walkthrough here of a few things I learned in dealing with printing in the XAML environment.
As always, this is not meant to be a ‘definitive guide’ or necessarily the ‘perfect way’ to implement functionality – it’s just some rough notes that I’m sharing in the interest of helping people along if they find this article.
Let’s say that I’ve got an app that displays some images. Perhaps I’ve got 20 images in a list and each has a title. I’m not going to display the UI for those on-screen, I’m only going to display them in print preview and on the printer.
I might want to offer the user a chance to print those images 1 per page, 4 per page, 16 per page and so on and I probably want a preview option for those choices as well.
Let’s get going. I’ll make a blank XAML project in Visual Studio.
and then I’ll stick a big button onto the screen;
and then I can put some code behind that to play around with and try things out. I’m not going to be overly fancy on the structuring of that code but I’ll hopefully get some printing done.
Registering for the Print Contract
If I run the code as it stands at the moment and bring in the devices charm, I won’t see much.
because we haven’t told the system that we can print anything. We need to do that by supporting the print contract. In a real app what I’ve done is to have a central abstracted service which implements this contract and then I have my various views register/de-register with that service to tell it about what content they have for printing.
However, in this little toy app I’ll write code to implement that contract when the button is clicked;
namespace PrintTest { using Windows.Graphics.Printing; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } void OnRegister(object sender, RoutedEventArgs e) { // Note, this manager can also invoke the print UI for us. PrintManager manager = PrintManager.GetForCurrentView(); manager.PrintTaskRequested += OnPrintTaskRequested; } void OnPrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args) { // If I need to be asynchronous, I can get a deferral. I don't *need* // to do this here, I'm just faking it. var deferral = args.Request.GetDeferral(); PrintTask printTask = args.Request.CreatePrintTask( "My Print Job", OnPrintTaskSourceRequestedHandler); printTask.Completed += OnPrintTaskCompleted; deferral.Complete(); } void OnPrintTaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args) { // TODO: Tidy up. } void OnPrintTaskSourceRequestedHandler(PrintTaskSourceRequestedArgs args) { // TODO: Make a print task source. } } }
Ok, so what did we do?
- We get the PrintManager and tell it that we can do some printing by adding our handler.
- When the PrintManager wants to print something, it will fire its event and we will create a PrintTask
- We add a blank handler for the Completed event on that PrintTask (not necessary but we might want it later)
- We add a callback (not an event handler) OnPrintTaskSourceRequestedHandler which is to be called when something actually needs printing or print previewing.
If you were to run up this code and click the button and set a few tracepoints on my new functions in my code to print out the function details then we would see output like;
Function: PrintTest.MainPage.OnRegister(object, Windows.UI.Xaml.RoutedEventArgs), Thread: 0x1ECC <No Name>
as we register by clicking the button. And then when I tap on the devices charm and then move back to Visual Studio to copy from the debugger I see;
Function: PrintTest.MainPage.OnPrintTaskRequested(Windows.Graphics.Printing.PrintManager, Windows.Graphics.Printing.PrintTaskRequestedEventArgs), Thread: 0x1D8C <No Name>
Function: PrintTest.MainPage.OnPrintTaskCompleted(Windows.Graphics.Printing.PrintTask, Windows.Graphics.Printing.PrintTaskCompletedEventArgs), Thread: 0x1540 <No Name>
Note – different threads to the one where we registered. And if I was to actually tap on a printer in that devices charm I’d see;
Providing Something to Print with PrintDocument
The way in which we provide something to print is by implementing that callback for OnPrintTaskSourceRequestHandler and handing back something that implements IPrintDocumentSource.
The first time I came across this in the XAML world I was a bit puzzled because I’d seen it in the HTML world and I knew that there was a way to ask the HTML DOM for one of these. I scratched my head for a while until I came across the PrintDocument class and its DocumentSource property.
So, the next thing to do is to make use of PrintDocument. Let’s do that in a simple way;
namespace PrintTest { using Windows.Graphics.Printing; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Printing; public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } void OnRegister(object sender, RoutedEventArgs e) { // Note, this manager can also invoke the print UI for us. PrintManager manager = PrintManager.GetForCurrentView(); manager.PrintTaskRequested += OnPrintTaskRequested; } void OnPrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args) { // If I need to be asynchronous, I can get a deferral. I don't *need* // to do this here, I'm just faking it. var deferral = args.Request.GetDeferral(); PrintTask printTask = args.Request.CreatePrintTask( "My Print Job", OnPrintTaskSourceRequestedHandler); printTask.Completed += OnPrintTaskCompleted; deferral.Complete(); } void OnPrintTaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args) { // TODO: Tidy up. } void OnPrintTaskSourceRequestedHandler(PrintTaskSourceRequestedArgs args) { // NB: this code does NOT work. this._document = new PrintDocument(); this._document.Paginate += OnPaginate; this._document.GetPreviewPage += OnGetPreviewPage; this._document.AddPages += OnAddPages; // Tell the caller about it. args.SetSource(this._document.DocumentSource); } void OnAddPages(object sender, AddPagesEventArgs e) { } void OnGetPreviewPage(object sender, GetPreviewPageEventArgs e) { } void OnPaginate(object sender, PaginateEventArgs e) { } PrintDocument _document; } }
and if I go and pepper this with breakpoints;
and then run up this code, hit the button to register for print and then use the devices charm to select a printer, none of my breakpoints will hit and the system reports back;
Now, what’s going on here is that creating the PrintDocument is throwing an exception and if I switch on Visual Studio’s trusty first-chance exception catching;
I can see it ( because my code doesn’t try to catch it right now );
Ah, RPC_E_WRONG_THREAD Like an old friend rising up to greet me. Now, every time I see this exception I thank the developer who bothered to put in the check that throws this exception rather than letting me solider on writing code in ignorance.
So, we need to do this on a different thread. Let’s do it on the UI thread;
namespace PrintTest { using System; using Windows.Graphics.Printing; using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Printing; public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } void OnRegister(object sender, RoutedEventArgs e) { // Note, this manager can also invoke the print UI for us. PrintManager manager = PrintManager.GetForCurrentView(); manager.PrintTaskRequested += OnPrintTaskRequested; } void OnPrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args) { // If I need to be asynchronous, I can get a deferral. I don't *need* // to do this here, I'm just faking it. var deferral = args.Request.GetDeferral(); PrintTask printTask = args.Request.CreatePrintTask( "My Print Job", OnPrintTaskSourceRequestedHandler); printTask.Completed += OnPrintTaskCompleted; deferral.Complete(); } void OnPrintTaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args) { // TODO: Tidy up. } async void OnPrintTaskSourceRequestedHandler(PrintTaskSourceRequestedArgs args) { var deferral = args.GetDeferral(); await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this._document = new PrintDocument(); this._document.Paginate += OnPaginate; this._document.GetPreviewPage += OnGetPreviewPage; this._document.AddPages += OnAddPages; // Tell the caller about it. args.SetSource(this._document.DocumentSource); } ); deferral.Complete(); } void OnAddPages(object sender, AddPagesEventArgs e) { } void OnGetPreviewPage(object sender, GetPreviewPageEventArgs e) { } void OnPaginate(object sender, PaginateEventArgs e) { } PrintDocument _document; } }
and if I then run that code through its paces then I hit my OnPaginate breakpoint;
Pagination
Ok, pagination could be a pretty complex process. I’ll come back later when I’ve got some real data but, for the moment, let’s get “Hello World” on the screen. So, I’ll simply say that I’ve got 1 page of content.
void OnPaginate(object sender, PaginateEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // I have one page and that's *FINAL* ! this._document.SetPreviewPageCount(1, PreviewPageCountType.Final); } ); }
and that all seems fine so I need to probably provide that content to print preview from OnGetPreviewPage so let’s try that;
namespace PrintTest { using System; using System.Collections.Generic; using Windows.Graphics.Printing; using Windows.UI; using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Printing; public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } void OnRegister(object sender, RoutedEventArgs e) { // Note, this manager can also invoke the print UI for us. PrintManager manager = PrintManager.GetForCurrentView(); manager.PrintTaskRequested += OnPrintTaskRequested; } void OnPrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args) { // If I need to be asynchronous, I can get a deferral. I don't *need* // to do this here, I'm just faking it. var deferral = args.Request.GetDeferral(); PrintTask printTask = args.Request.CreatePrintTask( "My Print Job", OnPrintTaskSourceRequestedHandler); printTask.Completed += OnPrintTaskCompleted; deferral.Complete(); } void OnPrintTaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args) { this._document = null; this._pages = null; } async void OnPrintTaskSourceRequestedHandler(PrintTaskSourceRequestedArgs args) { var deferral = args.GetDeferral(); await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this._document = new PrintDocument(); this._document.Paginate += OnPaginate; this._document.GetPreviewPage += OnGetPreviewPage; this._document.AddPages += OnAddPages; // Tell the caller about it. args.SetSource(this._document.DocumentSource); } ); deferral.Complete(); } void OnAddPages(object sender, AddPagesEventArgs e) { } void OnGetPreviewPage(object sender, GetPreviewPageEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // NB: assuming it's ok to keep all these pages in // memory. might not be the right thing to do // of course. if (this._pages == null) { this._pages = new Dictionary<int, UIElement>(); } if (!this._pages.ContainsKey(e.PageNumber)) { // Make a page. TextBlock textBlock = new TextBlock() { Text = string.Format("I am page {0}", e.PageNumber), FontSize = 32, Foreground = new SolidColorBrush(Colors.Black) }; this._pages[e.PageNumber] = textBlock; } this._document.SetPreviewPage(e.PageNumber, this._pages[e.PageNumber]); } ); } void OnPaginate(object sender, PaginateEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // I have one page and that's *FINAL* ! this._document.SetPreviewPageCount(1, PreviewPageCountType.Final); } ); } Dictionary<int, UIElement> _pages; PrintDocument _document; } }
and I’ve tried to tidy up the data that I’m keeping around a little in the completed event handler.
A word of caution about that completion handler. With a handler like that which resets things you may find that you hit scenarios like this in your debugger;
- You start to debug the printing code.
- You use the print charm.
- You drop to breakpoints in Visual Studio.
- The system abandons the print task causing your completed code to run and resetting state that you might be relying on in your debugging session.
In fact, debugging this code may well be one of those scenarios where the simulator or a 2nd machine might help.
If we exercise this code we get;
so that’s print previewing just fine but it doesn’t actually go to the printer. If I want to do that, I need to do something in OnAddPages such as;
void OnAddPages(object sender, AddPagesEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this._document.AddPage(this._pages[1]); this._document.AddPagesComplete(); } ); }
which works great and the document print previews and it prints but, unfortunately, it prints outside of the printable area on my printer so that the text is clipped at the top and at the left.
Printable Area Printing
I probably should have asked for the printable area when previewing. Let’s have another attempt at that – note I’m not 100% sure how DPI plays into this so take care here but this sorts out the clipping problem for me in this specific case;
namespace PrintTest { using System; using System.Collections.Generic; using Windows.Foundation; using Windows.Graphics.Printing; using Windows.UI; using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Printing; public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } void OnRegister(object sender, RoutedEventArgs e) { // Note, this manager can also invoke the print UI for us. PrintManager manager = PrintManager.GetForCurrentView(); manager.PrintTaskRequested += OnPrintTaskRequested; } void OnPrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args) { // If I need to be asynchronous, I can get a deferral. I don't *need* // to do this here, I'm just faking it. var deferral = args.Request.GetDeferral(); PrintTask printTask = args.Request.CreatePrintTask( "My Print Job", OnPrintTaskSourceRequestedHandler); printTask.Completed += OnPrintTaskCompleted; deferral.Complete(); } void OnPrintTaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args) { this._pageSize = null; this._imageableRect = null; this._document = null; this._pages = null; } async void OnPrintTaskSourceRequestedHandler(PrintTaskSourceRequestedArgs args) { var deferral = args.GetDeferral(); await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this._document = new PrintDocument(); this._document.Paginate += OnPaginate; this._document.GetPreviewPage += OnGetPreviewPage; this._document.AddPages += OnAddPages; // Tell the caller about it. args.SetSource(this._document.DocumentSource); } ); deferral.Complete(); } void OnAddPages(object sender, AddPagesEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this._document.AddPage(this._pages[1]); this._document.AddPagesComplete(); } ); } void OnGetPreviewPage(object sender, GetPreviewPageEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // NB: assuming it's ok to keep all these pages in // memory. might not be the right thing to do // of course. if (this._pages == null) { this._pages = new Dictionary<int, UIElement>(); } if (!this._pages.ContainsKey(e.PageNumber)) { // TBD: Unsure about DPI here. Canvas canvas = new Canvas(); canvas.Width = this._pageSize.Value.Width; canvas.Height = this._pageSize.Value.Height; // Make a page. TextBlock textBlock = new TextBlock() { Text = string.Format("I am page {0}", e.PageNumber), FontSize = 32, Foreground = new SolidColorBrush(Colors.Black) }; Canvas.SetLeft(textBlock, this._imageableRect.Value.Left); Canvas.SetTop(textBlock, this._imageableRect.Value.Top); canvas.Children.Add(textBlock); this._pages[e.PageNumber] = canvas; } this._document.SetPreviewPage(e.PageNumber, this._pages[e.PageNumber]); } ); } void OnPaginate(object sender, PaginateEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // I have one page and that's *FINAL* ! this.GetPageSize(e); this._document.SetPreviewPageCount(1, PreviewPageCountType.Final); } ); } void GetPageSize(PaginateEventArgs e) { if (this._pageSize == null) { PrintPageDescription description = e.PrintTaskOptions.GetPageDescription( (uint)e.CurrentPreviewPageNumber); this._pageSize = description.PageSize; this._imageableRect = description.ImageableRect; } } Size? _pageSize; Rect? _imageableRect; Dictionary<int, UIElement> _pages; PrintDocument _document; } }
and that output comes out nicely on my printer without the clipping but I’ll switch from that Canvas to a Grid later on and I haven’t thought anything here about the right hand side or the bottom of that page being clipped yet.
Adding Custom Options
When I finally get around to printing some images, I’d like the user to be able to choose a layout. For example, they could choose 1/4/9/16 images per page. How would I add custom options?
When the PrintTask is created, we can grab hold of its PrintTaskOptionDetails class and add our own option. For example – notice the code added here to OnPrintTaskRequested;
namespace PrintTest { using System; using System.Collections.Generic; using Windows.Foundation; using Windows.Graphics.Printing; using Windows.Graphics.Printing.OptionDetails; using Windows.UI; using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Printing; public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } void OnRegister(object sender, RoutedEventArgs e) { // Note, this manager can also invoke the print UI for us. PrintManager manager = PrintManager.GetForCurrentView(); manager.PrintTaskRequested += OnPrintTaskRequested; } void OnPrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args) { // If I need to be asynchronous, I can get a deferral. I don't *need* // to do this here, I'm just faking it. var deferral = args.Request.GetDeferral(); PrintTask printTask = args.Request.CreatePrintTask( "My Print Job", OnPrintTaskSourceRequestedHandler); printTask.Completed += OnPrintTaskCompleted; this.AddCustomPrintOption(printTask); deferral.Complete(); } void AddCustomPrintOption(PrintTask printTask) { PrintTaskOptionDetails details = PrintTaskOptionDetails.GetFromPrintTaskOptions( printTask.Options); // Clear, string "optionId" needs to be in a constant etc. PrintCustomItemListOptionDetails formatOptions = details.CreateItemListOption("optionId", "Images Per Page"); var options = new string[] { "1", "4", "9", "16" }; foreach (var item in options) { formatOptions.AddItem(item, item); } details.DisplayedOptions.Add("optionId"); details.OptionChanged += OnItemsPerPageChanged; } void OnItemsPerPageChanged(PrintTaskOptionDetails sender, PrintTaskOptionChangedEventArgs args) { if ((string)args.OptionId == "optionId") { PrintTaskOptionDetails optionDetails = (PrintTaskOptionDetails)sender; string value = optionDetails.Options["optionId"].Value as string; if (!string.IsNullOrEmpty(value)) { int itemsPerPage = 0; if (int.TryParse(value, out itemsPerPage)) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this._pages = null; this._itemsPerPage = itemsPerPage; this._document.InvalidatePreview(); } ); } } } } void OnPrintTaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args) { this._pageSize = null; this._imageableRect = null; this._document = null; this._pages = null; } async void OnPrintTaskSourceRequestedHandler(PrintTaskSourceRequestedArgs args) { var deferral = args.GetDeferral(); await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this._document = new PrintDocument(); this._document.Paginate += OnPaginate; this._document.GetPreviewPage += OnGetPreviewPage; this._document.AddPages += OnAddPages; // Tell the caller about it. args.SetSource(this._document.DocumentSource); } ); deferral.Complete(); } void OnAddPages(object sender, AddPagesEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this._document.AddPage(this._pages[1]); this._document.AddPagesComplete(); } ); } void OnGetPreviewPage(object sender, GetPreviewPageEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // NB: assuming it's ok to keep all these pages in // memory. might not be the right thing to do // of course. if (this._pages == null) { this._pages = new Dictionary<int, UIElement>(); } if (!this._pages.ContainsKey(e.PageNumber)) { // TBD: Unsure about DPI here. Canvas canvas = new Canvas(); canvas.Width = this._pageSize.Value.Width; canvas.Height = this._pageSize.Value.Height; // Make a page. TextBlock textBlock = new TextBlock() { Text = string.Format("I am page {0}", e.PageNumber), FontSize = 32, Foreground = new SolidColorBrush(Colors.Black) }; Canvas.SetLeft(textBlock, this._imageableRect.Value.Left); Canvas.SetTop(textBlock, this._imageableRect.Value.Top); canvas.Children.Add(textBlock); this._pages[e.PageNumber] = canvas; } this._document.SetPreviewPage(e.PageNumber, this._pages[e.PageNumber]); } ); } void OnPaginate(object sender, PaginateEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this.GetPageSize(e); // I have one page and that's *FINAL* ! this._document.SetPreviewPageCount(1, PreviewPageCountType.Final); } ); } void GetPageSize(PaginateEventArgs e) { if (this._pageSize == null) { PrintPageDescription description = e.PrintTaskOptions.GetPageDescription( (uint)e.CurrentPreviewPageNumber); this._pageSize = description.PageSize; this._imageableRect = description.ImageableRect; } } int _itemsPerPage; Size? _pageSize; Rect? _imageableRect; Dictionary<int, UIElement> _pages; PrintDocument _document; } }
Clearly, there’s a need for me to structure my code into some additional classes here to handle these print options but you hopefully get the idea and the print UI is now displaying;
Adding Some Data
Let’s add some ‘semi-real’ data. I’ll add 20 images to my project and I’ll add them in the Assets folder and I’ll call them (for convenience) img0 to img19.jpg;
and I’ll add a little class to represent my data ( no property change notification or anything fancy );
class ImageData { public string Title { get; set; } public string ImageUri { get; set; } }
And I’ll make a little List of those when my page loads, and keep it around for later use;
public sealed partial class MainPage : Page { List<ImageData> _imageData; public MainPage() { this.InitializeComponent(); this.Loaded += OnLoaded; } void OnLoaded(object sender, RoutedEventArgs e) { this._imageData = Enumerable.Range(0, 20).Select( i => new ImageData() { Title = string.Format("Image number {0}", i + 1), ImageUri = string.Format("ms-appx:///Assets/img{0}.jpg", i) } ).ToList(); }
I’ll also add a simple user control. On that control I’ll have an Image and a TextBlock. I’ll data-bind the Source of the image to a property called ImageUri and I’ll data-bind the Text of that TextBlock to a property called Title. Here’s the XAML for that control;
<UserControl x:Class="PrintTest.ImageUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:PrintTest" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Image Source="{Binding ImageUri}" Stretch="Fill" /> <Viewbox StretchDirection="DownOnly" HorizontalAlignment="Left" Grid.Row="1"> <TextBlock Grid.Row="1" FontSize="24" Foreground="Black" Text="{Binding Title}" /> </Viewbox> </Grid> </UserControl>
Re-Paginating
Ok, now that I have some data there’s a need to rework my pagination. Let me re-work that code to take account of the number of items that I have and the number of items per page that the user has selected;
void OnPaginate(object sender, PaginateEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this.GetPageSize(e); int itemCount = this._imageData.Count; this._pageCount = itemCount / this._itemsPerPage; if ((itemCount % this._itemsPerPage) > 0) { this._pageCount++; } this._document.SetPreviewPageCount(this._pageCount, PreviewPageCountType.Final); } ); }
Layout
Now, in my code that builds up a particular page I need to do some work. I’ll make a Grid and populate it with rows, columns and then I’ll put instances of my user control into the rows/columns with (hopefully) the right DataContext set on each one.
I’ve also chosen to stop caching pages in a dictionary as I think that was a bit memory intensive and I’ve also replaced the Canvas that I was using to try and offset content into the printable area with a Grid.
That’s quite a lot of changes, so here’s the listing as I have it;
namespace PrintTest { using System; using System.Collections.Generic; using System.Linq; using Windows.Foundation; using Windows.Graphics.Printing; using Windows.Graphics.Printing.OptionDetails; using Windows.UI; using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Printing; public class ImageData { public string Title { get; set; } public string ImageUri { get; set; } } public sealed partial class MainPage : Page { List<ImageData> _imageData; public MainPage() { this.InitializeComponent(); this._itemsPerPage = 1; this.Loaded += OnLoaded; } void OnLoaded(object sender, RoutedEventArgs e) { this._imageData = Enumerable.Range(0, 20).Select( i => new ImageData() { Title = string.Format("Image number {0}", i + 1), ImageUri = string.Format("ms-appx:///Assets/img{0}.jpg", i) } ).ToList(); } void OnRegister(object sender, RoutedEventArgs e) { // Note, this manager can also invoke the print UI for us. PrintManager manager = PrintManager.GetForCurrentView(); manager.PrintTaskRequested += OnPrintTaskRequested; } void OnPrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args) { // If I need to be asynchronous, I can get a deferral. I don't *need* // to do this here, I'm just faking it. var deferral = args.Request.GetDeferral(); PrintTask printTask = args.Request.CreatePrintTask( "My Print Job", OnPrintTaskSourceRequestedHandler); printTask.Completed += OnPrintTaskCompleted; this.AddCustomPrintOption(printTask); deferral.Complete(); } void AddCustomPrintOption(PrintTask printTask) { PrintTaskOptionDetails details = PrintTaskOptionDetails.GetFromPrintTaskOptions( printTask.Options); // Clear, string "optionId" needs to be in a constant etc. PrintCustomItemListOptionDetails formatOptions = details.CreateItemListOption("optionId", "Images Per Page"); var options = new string[] { "1", "4", "9", "16" }; foreach (var item in options) { formatOptions.AddItem(item, item); } details.DisplayedOptions.Add("optionId"); details.OptionChanged += OnItemsPerPageChanged; } void OnItemsPerPageChanged(PrintTaskOptionDetails sender, PrintTaskOptionChangedEventArgs args) { if ((string)args.OptionId == "optionId") { PrintTaskOptionDetails optionDetails = (PrintTaskOptionDetails)sender; string value = optionDetails.Options["optionId"].Value as string; if (!string.IsNullOrEmpty(value)) { int itemsPerPage = 0; if (int.TryParse(value, out itemsPerPage)) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this._pageCount = 0; this._itemsPerPage = itemsPerPage; this._document.InvalidatePreview(); } ); } } } } void OnPrintTaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args) { this._itemsPerPage = 1; this._pageCount = 0; this._pageSize = null; this._imageableRect = null; this._document = null; } async void OnPrintTaskSourceRequestedHandler(PrintTaskSourceRequestedArgs args) { var deferral = args.GetDeferral(); await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this._document = new PrintDocument(); this._document.Paginate += OnPaginate; this._document.GetPreviewPage += OnGetPreviewPage; this._document.AddPages += OnAddPages; // Tell the caller about it. args.SetSource(this._document.DocumentSource); } ); deferral.Complete(); } void OnAddPages(object sender, AddPagesEventArgs e) { // Note: this code does not take notice of any page range specified // by the user. It should. this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { for (int i = 1; i <= this._pageCount; i++) { this._document.AddPage(this.BuildPage(i)); } this._document.AddPagesComplete(); } ); } void OnGetPreviewPage(object sender, GetPreviewPageEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // NB: No longer caching these in a dictionary. this._document.SetPreviewPage(e.PageNumber, this.BuildPage(e.PageNumber)); } ); } UIElement BuildPage(int pageNumber) { // Account for pages going 1..N rather than 0..N-1 int pageIndex = pageNumber - 1; // TBD: Unsure about DPI here. // Changed from a Canvas in the previous code. Grid parentGrid = new Grid(); parentGrid.Width = this._pageSize.Value.Width; parentGrid.Height = this._pageSize.Value.Height; // Make a grid Grid grid = new Grid(); grid.Margin = new Thickness( this._imageableRect.Value.Left, this._imageableRect.Value.Top, this._pageSize.Value.Width - this._imageableRect.Value.Width - this._imageableRect.Value.Left, this._pageSize.Value.Height - this._imageableRect.Value.Height - this._imageableRect.Value.Top); // How many squares? int squareCount = (int)(Math.Sqrt(this._itemsPerPage)); // Make grid rows and cols GridLength length = new GridLength(1, GridUnitType.Star); for (int i = 0; i < squareCount; i++) { grid.RowDefinitions.Add(new RowDefinition() { Height = length }); grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = length }); } // Make items int startIndex = this._itemsPerPage * pageIndex; int endIndex = Math.Min(startIndex + this._itemsPerPage, this._imageData.Count); int rowNo = 0; int colNo = 0; for (int itemIndex = startIndex; itemIndex < endIndex; itemIndex++) { ImageUserControl control = new ImageUserControl(); control.DataContext = this._imageData[itemIndex]; Grid.SetRow(control, rowNo); Grid.SetColumn(control, colNo); colNo++; if (colNo >= squareCount) { colNo = 0; rowNo++; } grid.Children.Add(control); } // Offset it into the printable area Canvas.SetLeft(grid, this._imageableRect.Value.Left); Canvas.SetTop(grid, this._imageableRect.Value.Top); parentGrid.Children.Add(grid); return (parentGrid); } void OnPaginate(object sender, PaginateEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { this.GetPageSize(e); int itemCount = this._imageData.Count; this._pageCount = itemCount / this._itemsPerPage; if ((itemCount % this._itemsPerPage) > 0) { this._pageCount++; } this._document.SetPreviewPageCount(this._pageCount, PreviewPageCountType.Final); } ); } void GetPageSize(PaginateEventArgs e) { if (this._pageSize == null) { PrintPageDescription description = e.PrintTaskOptions.GetPageDescription( (uint)e.CurrentPreviewPageNumber); this._pageSize = description.PageSize; this._imageableRect = description.ImageableRect; } } int _itemsPerPage; int _pageCount; Size? _pageSize; Rect? _imageableRect; PrintDocument _document; } }
and I could possibly fool you into thinking that this was all just “working fine”. Take a look at the screen shots below;
that’s a print-preview of 1 image per page. Here’s 4;
and the problem becomes a little more obvious – where’s the other 3 images? Well, I actually have this problem even in single-page mode. Here’s what my first page really looks like when I first load it;
What’s going on here? The image isn’t loaded at the point where the preview is being built and so it’s too late by the time it shows up on screen.
It’s also worth noting that if I send this to a printer, e.g. the XPS Document Writer then I get no images in that print out. Here’s the view;
Fixing the Image Loading Problem
My first thought on how to resolve this was to attempt to handle the ImageOpened event on the Image that’s living inside of my user control. However, this event doesn’t seem to fire so in this scenario so I experimented with a few more options.
What I banged up against seemed to be a dilemma between;
- The image won’t try to load if I don’t pass it back to the system as part of the content to be printed or previewed. That is – if I don’t set the control tree containing the image into part of an actual preview page then it won’t attempt to load.
- If I do put the image into the control tree then if it hasn’t loaded already it’s too late.
I played with a few different ways of doing this. What I settled on was to take the responsibility of loading the image away from the Image itself and delegate it down to the BitmapImage class to see if I could get a little more control.
I changed my user control’s XAML;
<UserControl x:Class="PrintTest.ImageUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:PrintTest" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Image x:Name="imgScreen" Stretch="Fill"/> <Viewbox StretchDirection="DownOnly" HorizontalAlignment="Left" Grid.Row="1"> <TextBlock Grid.Row="1" FontSize="24" Foreground="Black" Text="{Binding Title}" /> </Viewbox> </Grid> </UserControl>
so that the Image no longer is data-bound but is, instead, named. I changed the code behind that control to add a new ImageUri property which could be data-bound;
public sealed partial class ImageUserControl : UserControl { public ImageUserControl() { this.InitializeComponent(); } public static DependencyProperty ImageUriProperty = DependencyProperty.Register("ImageUri", typeof(string), typeof(ImageUserControl), null); string ImageUri { get { return ((string)base.GetValue(ImageUriProperty)); } set { base.SetValue(ImageUriProperty, value); } } public async Task LoadAsync() { // Note, this is going to work from the app package, // not from the web - that's a different API call. var file = await StorageFile.GetFileFromApplicationUriAsync( new Uri(this.ImageUri)); var stream = await file.OpenReadAsync(); BitmapImage bitmapImage = new BitmapImage(); await bitmapImage.SetSourceAsync(stream); this.imgScreen.Source = bitmapImage; } }
and I added that LoadAsync method which attempts to async load the file into the BitmapImage stream and then set that as the Source for the Image.
I then changed the way in which I built pages such that I build them asynchronously, waiting for all the images to load before telling the system about them. My preview function changed;
void OnGetPreviewPage(object sender, GetPreviewPageEventArgs e) { this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => { // NB: No longer caching these in a dictionary. UIElement page = await this.BuildPageAsync(e.PageNumber); this._document.SetPreviewPage(e.PageNumber, page); } ); }
so I now await the building of a page and I only call SetPreviewPage when I have that page ready. My BuildPage function became BuildPageAsync;
async Task<UIElement> BuildPageAsync(int pageNumber) { // Account for pages going 1..N rather than 0..N-1 int pageIndex = pageNumber - 1; // TBD: Unsure about DPI here. // Changed from a Canvas in the previous code. Grid parentGrid = new Grid(); parentGrid.Width = this._pageSize.Value.Width; parentGrid.Height = this._pageSize.Value.Height; // Make a grid Grid grid = new Grid(); grid.Margin = new Thickness( this._imageableRect.Value.Left, this._imageableRect.Value.Top, this._pageSize.Value.Width - this._imageableRect.Value.Width - this._imageableRect.Value.Left, this._pageSize.Value.Height - this._imageableRect.Value.Height - this._imageableRect.Value.Top); // How many squares? int squareCount = (int)(Math.Sqrt(this._itemsPerPage)); // Make grid rows and cols GridLength length = new GridLength(1, GridUnitType.Star); for (int i = 0; i < squareCount; i++) { grid.RowDefinitions.Add(new RowDefinition() { Height = length }); grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = length }); } // Make items int startIndex = this._itemsPerPage * pageIndex; int endIndex = Math.Min(startIndex + this._itemsPerPage, this._imageData.Count); int rowNo = 0; int colNo = 0; List<Task> imageLoadTasks = new List<Task>(); for (int itemIndex = startIndex; itemIndex < endIndex; itemIndex++) { ImageUserControl control = new ImageUserControl(); Binding binding = new Binding() { Path = new PropertyPath("ImageUri") }; control.SetBinding(ImageUserControl.ImageUriProperty, binding); control.DataContext = this._imageData[itemIndex]; imageLoadTasks.Add(control.LoadAsync()); Grid.SetRow(control, rowNo); Grid.SetColumn(control, colNo); colNo++; if (colNo >= squareCount) { colNo = 0; rowNo++; } grid.Children.Add(control); } // Offset it into the printable area Canvas.SetLeft(grid, this._imageableRect.Value.Left); Canvas.SetTop(grid, this._imageableRect.Value.Top); parentGrid.Children.Add(grid); // Wait for the images to load. await Task.WhenAll(imageLoadTasks); return (parentGrid); }
and you’ll notice that I build up a List of Task around lines 39 and lines 51 and those tasks represent the async loading of those images into the ImageUserControl instances via the new LoadAsync() method I added.
When all those tasks are done (line 73) I will return the page.
My actual printing code had to change too to accommodate this new async way of building pages;
void OnAddPages(object sender, AddPagesEventArgs e) { // Note: this code does not take notice of any page range specified // by the user. It should. this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => { for (int i = 1; i <= this._pageCount; i++) { this._document.AddPage(await this.BuildPageAsync(i)); } this._document.AddPagesComplete(); } ); }
and this seemed to work out both in Print Preview;
and I can print to XPS with 1 per page;
and with (e.g.) 9 per page;
and that all seems to work ok and it comes out fine on my physical printer too.
The Code
Here’s the project for this one in case you want to download it. I took the images out though so all images now point to the logo in the project file.
Enjoy