Printing Reporting Services 2005 Reports

by Jacob 8. December 2007 16:21

Invoice 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.

WCF WTF?

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:

  1. Load the report. This is, quite frankly, the easiest call.
    Client.LoadReport(ReportLocation, null);
  2. 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;
                parameters.Add(paramValue);
            }
            Client.SetExecutionParameters(parameters.ToArray(), null);
        }
    }
  3. 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)
    {
        byte[] result;
        string ext, mimeType, encoding;
        Warning[] warnings;
        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);
        pages.Add(new Metafile(memStream));
    
        return pages;
    }
  4. 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);
        pages.Add(new Metafile(memStream));
    }

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:

<DeviceInfo><OutputFormat>emf</OutputFormat></DeviceInfo>

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

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();
    if (string.IsNullOrEmpty(printer))
    {
        throw new ArgumentException("A printer is required.");
    }

    printingPage = 0;
    PrintDocument pd = new PrintDocument();
    pd.PrinterSettings.PrinterName = printer;
    pd.PrintPage += new PrintPageEventHandler(pd_PrintPage);
    pd.Print();
}

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.

Wrapping Up

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:

<security mode="TransportCredentialOnly">
  <transport clientCredentialType="Windows" />
</security>

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.

Technorati Tags: ,,

Tags: , ,

Programming

Comments


 ltk 
March 25. 2008 13:03
ltk
Help!  The option in Crystal to select multiple parameters and end up with each parameter on a separate page as indicated in the start of this article invoices - I can't replicate that with Reporting Services.  I've been searching but find nothing.  This would be user run.  Any help?  Please...


March 25. 2008 23:47
Jacob
I couldn't do that with either Crystal or RS. I was doing it using Visual Studio and C# to access each report and printing them one at a time.


 spottedmahn 
December 12. 2008 14:01
spottedmahn
Thanks man, this helped me out a lot!


December 12. 2008 15:41
Jacob
That's awesome! I'm glad it was helpful.


May 11. 2009 01:31
Federico
This is great help! However, is there posible to post or send a test/client project that actually uses this clases? I'm trying to use them with my Reporting Services reports with no luck.
Thanks in advance for the help!


United States Kraig 
August 7. 2009 12:01
Kraig
I need to basically repeat the request Federico made "This is great help! However, is there posible to post or send a test/client project that actually uses this clases? I'm trying to use them with my Reporting Services reports with no luck.
Thanks in advance for the help!"

Thanks!

Kraig


August 7. 2009 20:46
Jacob
I agree. Those classes are part of a larger project, though, and extricating them would be... complex. It's on my list of things I'd like to do, but it isn't likely to be soon. Sorry about that. I'd be happy to help if you can email me details, though.


United States Sadia 
December 3. 2009 14:55
Sadia
Your post helped me out a lot. I am using your suggestions in my code. My task is to retrieve an SSRS report and print it from my code. My SSRS report has some special formatting especially font formatting. I am using a thrid party font to print on the report for security purposes. When I retrieve the Image back from the web service and print it the font formatting is lost. It all prints in Arial. It keeps the size and weight... but the fact that it was a different font it can't comprehend that back to me.

Any help is greatly appreciated.

Thanks,


December 3. 2009 15:17
Jacob
@Sadia: The problem is likely in how you think of "image" format. There are two different types of images and most people think they're all like bitmaps (i.e. defined as a series of pixels). The pixel formats get really big really fast for decent resolutions so I chose to use EMF. EMF sends "vector" data (I think that's the term, I'm not really an image geek) which means it sends objects and instructions on how to draw them instead of pixels.

The key piece of all that for you is that SSRS isn't sending an image of pixels, it's sending the text, location, and font data to your client which then does the rendering of that data into the final image that is printed. So if your client doesn't have the font installed, it will do its best to render that text and Arial tends to be the default in those situations.

At least, that's where I'd start looking.


Ireland Alan O Keeffe 
February 19. 2010 07:58
Alan O Keeffe
@Federico and @Kraig

To create a test cilent
1. Download Jacobs source code.
2. Open the project in Visual studio.
3. Add a console application to the solution in visual studio.
4. Add a reference from your console application to Jacobs project (ReportingServicesManager.)
5. Add the following code as the Main method in your console app, and then run teh console app.

        static void Main(string[] args)
        {
            ReportManager rm = new ReportManager();
            //YOUR report location
            rm.ReportLocation = "/APT.Reports/GroupDCSORP";
            
            Dictionary<string, string > parameters = new Dictionary<string,string>();

            //Add YOUR report parameters, if you have any
            parameters.Add("Currency", "en-IE");
            parameters.Add("Id", "1313056");
            parameters.Add("RunDate", "2010-01-01");

            List<Dictionary<string, string>> parameterList= new List<Dictionary<string, string>>();
            parameterList.Add(parameters);

            List<System.Drawing.Imaging.Metafile>[] metafiles = rm.RunMultiple(parameterList);

            PrintManager pm = new PrintManager();

            //PrinterSettings.InstalledPrinters will get a list of all installed printer names
            //PrinterSettings.StringCollection printers = PrinterSettings.InstalledPrinters;
            //pm.Printer = printers[0];
            //YOUR printer name
            pm.Printer = "HP LaserJet 4050 Series PCL 5";
            pm.EmfImage = metafiles[0];

            pm.Print();

        }


February 19. 2010 12:52
Jacob
Nice addition, Alan. Thanks!

Comments are closed

Information

    Recent Posts

    Calendar

    <<  September 2010  >>
    MoTuWeThFrSaSu
    303112345
    6789101112
    13141516171819
    20212223242526
    27282930123
    45678910

    View posts in large calendar
    Disclaimer
    The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

    © Copyright 2010 Scruffy-looking Cat Herder