Printing Reports Programmatically Using C# and SQL Server 2000 Reporting Services

Summary: Learn a technique for printing reports programmatically using the Reporting Services XML Web service and C#.

Microsoft SQL Server 2000 Reporting Services is Microsoft's latest entry into the Business Intellegence marketplace and it joins a host of other BI products for SQL Server 2000. Reporting Services is a new, server-based reporting platform that you can use to create and manage tabular, matrix, graphical, and free-form reports that contain data from relational and multidimensional data sources. The reports that you create can be viewed and managed over a Web-based connection. As a developer, you have several programming opportunities available to you through the Reporting Services API. One of the most appealing aspects of Reporting Services is the open and flexible Web service API (also known as the SOAP API) which enables you to integrate existing features and capabilities of Reporting Services into your own custom reporting and management tools for Web sites and Windows applications. The SOAP API consists of close to one-hundred different XML Web service methods that you can use to integrate anything from report management to report rendering and execution into your custom applications. In this article, I will focus on one programming technique in particular: programmatically rendering a report and then sending that report directly to a local or network printer using C# and the Reporting Services Web service.

Before reading on, you can view the source code at the end of this article. The source code assumes you are creating a simple console application to test your print functionality and your ability to successfully call the Reporting Services Web service.

Creating a Reference to the Web service

The first thing you need to do is add a Web reference to the Reporting Services Web service in your development project that points to your report server. This can be done by right-clicking on your project in Visual Studio and choosing Add Web reference... . You should add a reference to a local report server (localhost) at "https://localhost/reportserver/reportservice.asmx". If you have a remote report server, simply change the URL of the Web reference. The end point for any Reporting Services Web service is "https://servername/reportserver/reportservice.asmx". For you Web service enthusiasts, you can access the end point through your browser with the ?wsdl directive to see the Web Service Description Language (WSDL) for the Reporting Services Web service (https://servername/reportserver/reportservice.asmx?wsdl).

The Render Method

Once you have added the appropriate Web reference, you will have all of the Web methods at your disposal. The Web methods are methods of the ReportingService class of the Web service proxy. To access the Web methods, you need to instantiate a ReportingService object and set credentials. A sample of this looks like the following:

 // Create proxy object and authenticate        
Console.WriteLine("Authenticating to the Web service...");
rs = new ReportingService();
rs.Credentials = System.Net.CredentialCache.DefaultCredentials;

Once you have created a proxy object for the Web service, you can access the methods as you would any normal C# class. The method that we are most interested in for the purpose of this article is the ReportingService.Render method. This is the primary method for rendering reports that have been published to the report server. The syntax for the Render method is as follows:

 public Byte[] Render(
string Report,
string Format,
string HistoryID,
string DeviceInfo,
[Namespace].ParameterValue[] Parameters,
[Namespace].DataSourceCredentials[] Credentials,
string ShowHideToggle,
out string Encoding,
out string MimeType,
out [Namespace].ParameterValue[] ParametersUsed,
out [Namespace].Warning[] Warnings
out string[] StreamIds);
Member of [Namespace].ReportingService

For more details about the various method arguments, see Reporting Services Books Online.

In code, you need to render to the Image output format designated by the Format argument. The value of this parameter is a string, simply "IMAGE". To get Enhanced Meta File (EMF) output, the kind of output you will need for printing, you also need to specify device information for the call to Render. That device information should be passed as an XML string for the DeviceInfo argument and should look like "<DeviceInfo><OutputFormat>EMF</OutputFormat></DeviceInfo>". The Render method returns the report as a base 64-encoded byte array. This array of bytes can be used to perform a number of functions including saving the output to a file on the hard drive, or more importantly, sending the bytes as a stream to a printer.

The sample code at the end of this article renders one of the report samples that ships with Reporting Services: the Company Sales sample report. Once you have effectively used the Render method to render a report, you can begin to think about how to print that report programmatically.

Printing the Report Programmatically

There are a few key challenges here in order to be able to print a report successfully using C# and Reporting Services. One challenge is figuring out how to print in the .NET Framework using C#. In the sample code included with this article, I use the classes of the System.Drawing.Printing namespace in order to send EMF output to a printer. The source code can show you how it all comes together. From a Reporting Services standpoint, the key challenge is determining how many pages there are in the report. The SOAP API for Reporting Services lacks the ability to evaluate a report's number of printed pages through any exposed report properties. So the trick is to determine how many pages there are through some other means. Fortunately, the SOAP API does return an array of stream IDs whenever Render is called. What are these stream IDs you ask? Well, when EMF output is selected, the Render method returns the results of the rendered report as a byte array, but it only sends back the first page. Subsequent pages are associated with the report as streams with accompanying stream IDs. By counting the number of StreamIDs in the resultant string array, you can determine how many pages are in the report. The following code should give you the number of pages:

 // The total number of pages of the report is 1 + the streamIDs
int m_numberOfPages = streamIDs.Length + 1;

Once you know how many pages you are dealing with you can call the Render method for each page in the report and send that page to the printer. You can render specific pages using device information. The device information for this is StartPage. In the sample code, the device information for each subsequent call to render (that is each call after the original one) looks like "<DeviceInfo><OutputFormat>EMF</OutputFormat><StartPage>current page </StartPage></DeviceInfo>". After each page is rendered, you load the page (set of rendered bytes) into an array of pages, a multi-dimensional array of bytes, and process that array. For each byte array, you generate a memory stream and load an image of that memory stream into the print document. From there you can print each page. Printing using this technique is not for beginners, so you may want to study the source code and consult your .NET Framework Developers Guide for more information.

One more thing to remember is that you will need to replace the printer name placeholder in the source code with a valid printer name for your system:

 static void Main(string[] args)
{
   PrintExample pe = new PrintExample();
   pe.PrintReport(@"PrinterName");
}

Okay so get your hands dirty with some code, that is really the whole point of this article anyway! Seriously, I hope this provided you with some insight into printing reports programmatically using C# and Reporting Services. The Web service and its methods expose a rich set of operations that enable you to draw on the complete functionality of the report server and allow you to create custom tools for any part of the report life cycle, from management to execution. I highly encourage you to not only get involved in programming with XML Web services, but to experiment with tools that harness the power of the Reporting Services Web service and start integrating reporting into your current development projects.

Source Code (app.cs)

 /*=====================================================================

  File:      app.cs
  
  Summary:  A simple console application that demonstrates one way to
            print Reporting Services reports to a printer.

------------------------------------------------------------------------

 This code is provided "AS IS" with no warranties. Do not use this code
 in your production environment. This sample is not endorsed by Microsoft
 and does not represent production quality code.

======================================================================== */
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Printing;
using System.IO;
using System.Web.Services.Protocols;
using PrintReport.reportserver;
using System.Runtime.InteropServices; // For Marshal.Copy
namespace PrintReport
{
    /// <summary>
    /// A simple console application that demonstrates one way to
    /// print Reporting Services reports to a printer.
    /// </summary>
    class app
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
         static void Main(string[] args)
         {
            PrintExample pe = new PrintExample();
            // The name of the printer should be added here;
            // this could be a local or network printer.
            pe.PrintReport(@"PrinterName");
         }
    }
   class PrintExample
   {
      ReportingService rs;
      private byte[][] m_renderedReport;
      private Graphics.EnumerateMetafileProc m_delegate = null;
      private MemoryStream m_currentPageStream;
      private Metafile m_metafile = null;
      int m_numberOfPages;
      private int m_currentPrintingPage;
      private int m_lastPrintingPage;
                  
      public PrintExample()
      {
         // Create proxy object and authenticate
         Console.WriteLine("Authenticating to the Web service...");
         rs = new ReportingService();
         rs.Credentials = System.Net.CredentialCache.DefaultCredentials;
      }
      public byte[][] RenderReport(string reportPath)
      {
         // Private variables for rendering
         string deviceInfo = null;
         string format = "IMAGE";
         Byte[] firstPage = null;
         string encoding;
         string mimeType;
         Warning[] warnings = null;
         ParameterValue[] reportHistoryParameters = null;
         string[] streamIDs = null;
         Byte[][] pages = null;
                                             
         // Build device info based on the start page
         deviceInfo = 
            String.Format(@"<DeviceInfo><OutputFormat>{0}</OutputFormat></DeviceInfo>", "emf");
            
         //Exectute the report and get page count.
         try
         {
            // Renders the first page of the report and returns streamIDs for 
            // subsequent pages
            firstPage = rs.Render(
               reportPath, 
               format, 
               null, 
               deviceInfo,
               null,
               null,
               null,
               out encoding,
               out mimeType,
               out reportHistoryParameters, 
               out warnings, 
               out streamIDs);
            // The total number of pages of the report is 1 + the streamIDs         
            m_numberOfPages = streamIDs.Length + 1;
            pages = new Byte[m_numberOfPages][];
            
            // The first page was already rendered
            pages[0] = firstPage;
            
            for (int pageIndex = 1; pageIndex < m_numberOfPages; pageIndex++)
            {
               // Build device info based on start page
               deviceInfo = 
                  String.Format(@"<DeviceInfo><OutputFormat>{0}</OutputFormat><StartPage>{1}</StartPage></DeviceInfo>", 
                    "emf", pageIndex+1);
               pages[pageIndex] = rs.Render(
                  reportPath, 
                  format, 
                  null, 
                  deviceInfo,
                  null,
                  null,
                  null,
                  out encoding,
                  out mimeType,
                  out reportHistoryParameters, 
                  out warnings, 
                  out streamIDs);
            }
         }
         catch (SoapException ex)
         {
            Console.WriteLine(ex.Detail.InnerXml);
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message); 
         }
         finally
         {
            Console.WriteLine("Number of pages: {0}", pages.Length);
         }
         return pages;
      }
      
      public bool PrintReport(string printerName)
      {
         this.RenderedReport = this.RenderReport("/SampleReports/Company Sales");
         try
         {  
            // Wait for the report to completely render.
            if(m_numberOfPages < 1)
               return false;
            PrinterSettings printerSettings = new PrinterSettings();
            printerSettings.MaximumPage = m_numberOfPages;
            printerSettings.MinimumPage = 1;
            printerSettings.PrintRange = PrintRange.SomePages;
            printerSettings.FromPage = 1;
            printerSettings.ToPage   = m_numberOfPages;
            printerSettings.PrinterName = printerName;
            PrintDocument pd = new PrintDocument();
            m_currentPrintingPage = 1;
            m_lastPrintingPage = m_numberOfPages;
            pd.PrinterSettings = printerSettings;
            // Print report
            Console.WriteLine("Printing report...");
            pd.PrintPage += new PrintPageEventHandler(this.pd_PrintPage);
            pd.Print();
         }
         catch(Exception ex) 
         {
            Console.WriteLine(ex.Message);
         }
         finally
         {
            // Clean up goes here.
         }
         return true;
      }
      private void pd_PrintPage(object sender, PrintPageEventArgs ev)
      {
         ev.HasMorePages = false;
         if (m_currentPrintingPage <= m_lastPrintingPage && MoveToPage(m_currentPrintingPage))
         {
            // Draw the page
            ReportDrawPage(ev.Graphics);
            // If the next page is less than or equal to the last page, 
            // print another page.
            if (++m_currentPrintingPage <= m_lastPrintingPage)
               ev.HasMorePages = true;
         }
      }
      
      // Method to draw the current emf memory stream 
      private void ReportDrawPage(Graphics g)
      {
         if(null == m_currentPageStream || 0 == m_currentPageStream.Length || null ==m_metafile)
            return;
         lock(this)
         {
            // Set the metafile delegate.
            int width = m_metafile.Width;
            int height= m_metafile.Height;
            m_delegate = new Graphics.EnumerateMetafileProc(MetafileCallback);
            // Draw in the rectangle
            Point destPoint = new Point(0, 0);
            g.EnumerateMetafile(m_metafile,destPoint , m_delegate);
            // Clean up
            m_delegate = null;
         }
      }
      private bool MoveToPage(Int32 page)
      {
         // Check to make sure that the current page exists in
         // the array list
         if(null == this.RenderedReport[m_currentPrintingPage-1])
            return false;
         // Set current page stream equal to the rendered page
         m_currentPageStream = new MemoryStream(this.RenderedReport[m_currentPrintingPage-1]);
         // Set its postion to start.
         m_currentPageStream.Position = 0;
         // Initialize the metafile
         if(null != m_metafile)
         {
            m_metafile.Dispose();
            m_metafile = null;
         }
         // Load the metafile image for this page
         m_metafile =  new Metafile((Stream)m_currentPageStream);
         return true;
      }
      private bool MetafileCallback(
         EmfPlusRecordType recordType,
         int flags,
         int dataSize,
         IntPtr data,
         PlayRecordCallback callbackData)
      {  
         byte[] dataArray = null;
         // Dance around unmanaged code.
         if (data != IntPtr.Zero)
         {
            // Copy the unmanaged record to a managed byte buffer 
            // that can be used by PlayRecord.
            dataArray = new byte[dataSize];
            Marshal.Copy(data, dataArray, 0, dataSize);
         }
         // play the record.      
         m_metafile.PlayRecord(recordType, flags, dataSize, dataArray);
            
         return true;
      }
      public byte[][] RenderedReport
      {
         get 
         {
            return m_renderedReport;
         }
         set
         {
            m_renderedReport = value;
         }
      }
   }
}