I have had a project on my “I should build that” list for several years. I do volunteer work with the Racine/Kenosha Wisconsin Friends of the NRA. Every year, we (and many other FNRA groups across the USA) host an event that raises money to support the shooting sports community in Wisconsin. This money funds range improvements, youth hunting programs, events for veterans, and other things the local shooting sports community needs. Part of the fund raising is a raffle we do every year. To run the raffle, we print the tickets early and sell them in the months leading up to our big banquet in October. The ticket printing gig fell to me since I grabbed the job.
For the past 2 years, I handled this job by printing the tickets from an Excel spreadsheet. I laid things out just so and was able to rifle through all the tickets, one page at a time, in about 90 minutes. I regularly messed up several sheets during the printing because I would miss a number here or there. I looked at applications to print the tickets and thought “That looks easy. I should just build that.” 2010 printing season arrived and I finally did. Given that Silverlight now supports printing and I hadn’t tried anything interesting in printing reports with it, yet, I decided to build an Internet capable application to support my needs.
A few words first: this application supports my needs for tickets and mine alone. I needed to print a raffle ticket that had the following characteristics:
1. Prints to an 8.5″ x 11″ piece of paper.
2. Has 8 tickets per page.
3. Has the perforations where my sheets had perforations.
4. Prints a specific header for each stub.
5. Prints a user customized description to each ticket body.
6. Handles multipage printing.
This code is guaranteed to work on a Canon MP780 printer, because that is what I own. YMMV if you own a different printer. The app looks like this:
While printing these tickets, I found a the program refuses to handle documents larger than 34 pages or 272 tickets. This seems to be limited because Silverlight printing generates bitmaps which tend to be space intensive. At 34 pages, I’m seeing the print spool show 2GB+ of memory for the spooled job. My print server is a 32-bit Windows Home Server box-a 64-bit machine may do better.
The code
Adding a print handler to Silverlight is pretty easy. You need to make sure that printing is initiated from a user action with no other UI intervening in between the button click and the presentation of the print dialog. For example, you can’t add a MessageBox.Show(“Hi”) in between the button click and the creation of the print job. To handle the printing, you just need to create a System.Windows.Printing.PrintDocument, hook the PrintPage event, and go.
private void Button_Click(object sender, RoutedEventArgs e) { var doc = new PrintDocument(); var ticket = (Ticket) LayoutRoot.DataContext; var pageStart = ticket.FirstTicket; doc.PrintPage += (s, eventArgs) => { eventArgs.HasMorePages = (pageStart + 8) < ticket.LastTicket; eventArgs.PageVisual = GeneratePage(ticket, pageStart, eventArgs); pageStart += 8; }; doc.Print("Tickets"); }
PrintPage passes a sender object and a PrintPageEventArgs instance to the event handler. PrintPageEventArgs has two propertiees that are important: HasMorePages and PageVisual. Set HasMorePages to true if the PrintPage event should be called again. If the code is printing the last page, set HasMorePages to false. The PageVisual property tells the runtime what should be printed. Any valid descendent of UIElement works here.
While doing this printing, I tried a few approaches before finding one that worked.Please note that I stopped writing code as soon as this thing worked. I first tried creating the entire page of tickets via code and no XAML assistance. Setting up the tickets went pretty well. I had a number of issues putting the exact correct transform on the ticket description so that it would rotate 90 degrees and appear in the right spot on each ticket. I was regularly off by a fraction of an inch on one ticket or another. After futzing with this for an hour, I tried setting up the entire page. This got to be hard because the data binding code and little adjustments were killing a lot of time. I believe I could have made either approach work eventually, but I wasn’t that interested in the puzzle to go that far.
My final solution was a combination of all in code and all in XAML: I designed a user control called ticket. You can see the ticket user control in the screen shot above. This had a few advantages:
1. I was able to add WYSIWYG display of the tickets to the user, so that surprises are minimized when printing tickets.
2. I could get the orientation of the bound data perfect in Expression Blend.
3. This was up and running in about 15 minutes from the time I started designing the ticket until it worked as a printout.
The layout code is handled by GeneratePage. I used StackPanels to hold the tickets and simplify the layout calculations. I used data binding to set the ticket numbers and ticket text. The one tricky part of the layout was taking account of the left and right margins. The tickets don’t change size, but the print area does for the tickets on the left and right side of the page. You’ll see that for columns 0 and 3, the code handles the edges. You’ll also notice that the following code is single purpose-it only works for my ticket sheet.
private static UIElement GeneratePage(Ticket ticket, int pageStart, PrintPageEventArgs args) { double gridHeight = args.PrintableArea.Height; double gridWidth = args.PrintableArea.Width + args.PageMargins.Left + args.PageMargins.Right; var retval = new StackPanel { Height = gridHeight, Width = gridWidth, Orientation = Orientation.Vertical }; for (var i = 0; i < 2; ++i) { // Add a StackPanel that has Horizontal layout var rowPanel = new StackPanel { Height = gridHeight/2, Width = args.PrintableArea.Width, Orientation = Orientation.Horizontal }; retval.Children.Add(rowPanel); // Add columns for (var j = 0; j < 4; ++j) { if (pageStart > ticket.LastTicket) { break; } var stackPanel = new StackPanel { Orientation = Orientation.Vertical, Height=rowPanel.Height, }; rowPanel.Children.Add(stackPanel); switch (j) { case 0: stackPanel.Width = gridWidth/4 - args.PageMargins.Left; break; case 3: stackPanel.Width = gridWidth/4 - args.PageMargins.Right; break; default: stackPanel.Width = gridWidth/4; break; } var ticketControl = new TicketControl { LayoutRoot = { DataContext = new SimpleTicket { Text = ticket.TicketText, TicketLabel = string.Format("No. {0:000}",
pageStart)
}
}
};
stackPanel.Children.Add(ticketControl);
++pageStart;
}
}
return retval;
}
Yes, I realize I could have bought their software for $30 and saved myself the time of writing this application. But, I wanted to learn how to use the print functionality in Silverlight, and $30 would not have taught me the stuff I learned by doing.
If you want to try out the application, I’ve put it up over here. Source code (Silverlight 4.0 RC) is here. Have fun!