About a year ago, I had the "opportunity" to automate batch printing for a couple of reports for my small company. Printing an invoice and a packing slip for 100+ orders at a time practically begs to be automated. Now, because we have specific needs with regards to the order they print in and what gets stapled to what else, this wasn’t something you could build into the reports themselves. Because the reports were originally programmed in Crystal Reports, I descended into the Crystal cesspit and just made it happen. If you’ve ever tried to automate report data access and printing in CR, you might know some of the pain I found there.
Well, we’ve been working on kicking Crystal out. Reporting Services has its quirks but for the most part it has been a huge relief to be able to handle reports in a central server with scheduled delivery in multiple formats without having to deal with Crystal’s (sorry, "Business Object’s") quirky proprietariness, temperamental processing, or extortionate enterprise server. Anyway, delving into automating report printing in Reporting Services revealed something of a blind spot in that service—there’s no built-in way to print a report on the server.
Automating report printing for RS is still ten times easier than it was in CR. Even so, it’s a pain. Google wasn’t much help in slaying this particular dragon, though there’s a blog post by Bryan Keller from February 2004 about using C# to print RS 2000 reports that turned out to be useful. It turns out that printing is a tough nut to crack, starting with who is responsible for kicking off the print job (the client, the report server, or a print server). It’s not really surprising that RS decided to punt on this one.
A Solution (of sorts)
I ended up using the RS web service to generate the report and send it to me in EMF format. I’m using VS 2008 now, so this was a good time to test some of that new-fangled technology I’d heard so much about.
My first hurdle was trying to use Windows Communication Foundation to access the web service. This isn’t actually my first project using WCF—it’s my second. My first project was against the Dynamics web service for Great Plains, which is where I learned that WCF is tricky when using integrated windows security. I thought that I had slain that beast, but the configuration options I worked out for the GP Web Service flat-out didn’t work when hitting against the RS web service. I got authentication errors until I was pulling my hair out. (See update below)
I wish I could tell you how I managed to overcome the problem, but I can’t. I dropped back to using old-school web service objects. This is an easy option to miss because when adding a Service Reference you have to hit the "Advanced..." button and then notice the "Add Web Reference..." button from there.
I regret this necessity because the WCF generated objects allow you to keep track of your RS session manually by obtaining and passing a session ID. The web service objects don’t expose that those methods and that’s a shame. There’s probably a way to reconcile this feature disparity either by figuring out how to get WCF to work or by finding the right buttons to push on the web service configuration, but this simply wasn’t that high a priority for me. This isn’t going to be an application that more than one person uses at a time and the volume will be frankly low so I can let RS keep track of the session by IP if it wants to (it’s a mostly unfounded assumption that this is how RS keeps track of your session. I saw one reference to something that looked like it was tracking me by IP, but again it wasn’t a high-enough priority for me to track it down to be sure).
I Can Print That Report in Four Calls
So here’s the messages needed (roughly) for pulling a report from RS over the web service:
- Load the report. This is, quite frankly, the easiest call.
- Set the parameters. This one isn’t that hard either.
private void setParameters(Dictionary<string, string> reportParameters)
if (reportParameters.Count > 0)
List<ParameterValue> parameters = new List<ParameterValue>();
foreach (KeyValuePair<string, string> param in reportParameters)
ParameterValue paramValue = new ParameterValue();
paramValue.Name = param.Key;
paramValue.Value = param.Value;
- Run the report. Again, not a tough call, but if your format is an EMF image format (and mine is), then this only returns the first page. Additional page references are given in the StreamIds parameter which let you retrieve them separately.
private List<Metafile> renderReport(out string streamIds)
string ext, mimeType, encoding;
result = Client.Render(Settings.Default.ReportFormat, Settings.Default.DeviceInfo, out ext, out mimeType
, out encoding, out warnings, out streamIds);
List<Metafile> pages = new List<Metafile>();
MemoryStream memStream = new MemoryStream(result);
- Retrieve the extra pages. Nothing exotic once you know it has to be done. Call for each StreamId returned in the last call.
private void renderStream(string streamId, List<Metafile> pages)
string mimeType, encoding;
byte result = Client.RenderStream(Settings.Default.ReportFormat, streamId, Settings.Default.DeviceInfo
, out encoding, out mimeType);
MemoryStream memStream = new MemoryStream(result);
You can see that I put the requested format and the DeviceInfo into my app.config. The format is simply "IMAGE", and the DeviceInfo only indicates EMF as the format:
Note that even though that’s an XML fragment, it’s passed as a raw string, so there’s no need to get fancy.
I originally went with TIFF as the format, but ran into some issues that are big enough I’ll go into it here to spare you some pain. TIFF seems like a better format because it will send you the entire report in a single pass and thus allow you to cut the number of calls significantly for multi-page reports. The problem with TIFF is that a) printing multi-page TIFFs in .Net is a pain and b) the file size is huge. The default image resolution for TIFF is a measly 96dpi so you have to add how nice you want it to look in the <DeviceInfo> tag. Since my reports have barcodes in them, I needed some significant dpi (I didn’t get decent results until about 2400). That additional dpi comes with a huge file size hit such that transferring the image took a couple seconds for even a single page. EMF as a format transfers the image as drawing vector information so you can take care of scaling on the client. That makes EMF orders of magnitude smaller (and hence faster once all is said and done).
I ended up stuffing all of this in a single class (ReportManager) that I’ll link to at the bottom of this post. I’ll actually link the whole project because I was smart enough to encapsulate this stuff for reuse.
Printing turned out to be a whole lot easier than I expected using GDI+. Bryan Keller’s article referenced above was helpful but I didn’t need half of his complexity. After populating class variables "emfImage" as a List<MetaFile> and "printer" as a simple string you end up with this (yes, I know I’m abusing ArgumentException).
public void Print()
if (emfImage == null || emfImage.Count <= 0)
throw new ArgumentException("An image is required to print.");
printer = printer.Trim();
throw new ArgumentException("A printer is required.");
printingPage = 0;
PrintDocument pd = new PrintDocument();
pd.PrinterSettings.PrinterName = printer;
pd.PrintPage += new PrintPageEventHandler(pd_PrintPage);
private void pd_PrintPage(object sender, PrintPageEventArgs e)
Metafile page = emfImage[printingPage];
e.Graphics.DrawImage(page, 0, 0, page.Width, page.Height);
e.HasMorePages = ++printingPage < emfImage.Count;
I ran into EMF scaling issues when I tried to skip including the width and height on the call to DrawImage, I’m not sure why. Again, I stuffed all this into its own class (PrintManager), though I didn’t bother isolating it in a separate library project.
I’ve put both the project source and just the binaries up for download. Note that I never actually tested the RunMultiple methods as they weren’t needed for this project. Yeah, that’s sloppy of me and if this were a commercial project or intended to be absolutely stable I’d have been more thorough.
* UPDATE: Okay, I couldn't leave the WCF thing alone—I don't like being beaten by a mere computer. As usual, finding the eventual answer always makes me feel pretty stupid, and this is no exception. The key component (after getting the client configuration correct in <basicHttpBinding> as this:
<transport clientCredentialType="Windows" />
was to set the right Token ImpersonationLevel:
service.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
Once that was taken care of, things worked out much better.