Friday 16 September 2011

Print SSRS Report using ReportExecutionService



This post explains about programmatically rendering a report and then sending that report directly to a local or network printer using C# and the Reporting Services Web service.
PreRequisite : You must have SSRS(SQL Server Reporting Services) installed so that you can use Reporting web services provided by SSRS.

Step 1: Add Reference to 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. 
To do so right-click on your project in Visual Studio and choose Add Service reference... .Then Click on Advanced  and then "Add Web Reference.." as shown in below image..



Provide report server URL (in my case http://localhost/reportserver/ReportExecution2005.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 http://servername/reportserver/reportservice.asmx.
Provide service reference name(i provided "ExecutionService" as reference name) and click on Add.

now service reference gets added to user solution as shown below







Step 2: Using ReportingService.Render() method to Render report in desired format:
Once you have added the appropriate Web reference,  create a proxy object for the Web service, so that you can access the its methods.
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);

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.


Step 3:Printing the Report Programmatically
Here we use the classes of the System.Drawing.Printing namespace in order to send EMF output to a printer
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.

The MSDN documentation for Render method says that it 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.
But i think we get this StreamIDs as empty array everytime .....so we have to use another logic to find number of pages and their stream .....you can find this logic in my below code :

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.

i have used PrintDocument class to print the report...

Following is the code to achieve this functionality...


using System;
using System.Web;
using System.Configuration;
using System.Drawing.Printing;
using System.Drawing;
using System.IO;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using DirectlyPrintReport.ExecutionService;
using System.Security.Principal;



namespace DirectlyPrintReport
{

    public class PrintManager   
{

// class members
private ReportExecutionService rs = new ReportExecutionService();

private byte[][] m_renderedReport;
private Graphics.EnumerateMetafileProc m_delegate = null;
private MemoryStream m_currentPageStream;
private Metafile m_metafile = null;
private int m_numberOfPages;
private int m_currentPrintingPage;
private int m_lastPrintingPage;

public PrintManager()
{
rs.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
}


 public  PrintManager(System.Net.NetworkCredential credentials)
{
rs.Credentials = credentials;
}

public bool PrintReport(string reportPath, ExecutionService.ParameterValue[] parameters, int fromPage, int toPage)
        {
            this.m_renderedReport = this.RenderReport(reportPath, parameters);
            try
            {
                // Wait for the report to completely render.
                if (m_numberOfPages < 1)
                {
                    return false;
                }

                //You can set the Required Printer Settings(Paper Size, Page Source, Orientation etc) in printerSettings object defined below
                PrinterSettings printerSettings = new PrinterSettings();
                printerSettings.MaximumPage = m_numberOfPages;
                printerSettings.MinimumPage = 1;
                printerSettings.PrintRange = PrintRange.AllPages;//PrintRange.SomePages;

                PrintDocument pd = new PrintDocument();
                if (toPage != -1 && fromPage != -1)
                {
                    m_currentPrintingPage = fromPage;
                    m_lastPrintingPage = toPage;
                    if (m_numberOfPages < toPage)
                    {
                        toPage = m_numberOfPages;
                        m_lastPrintingPage = toPage;
                    }
                    if (m_numberOfPages < fromPage)
                    {
                        fromPage = m_numberOfPages;
                        m_currentPrintingPage = fromPage;
                    }
                    printerSettings.FromPage = fromPage;
                    printerSettings.ToPage = toPage;

                    Console.WriteLine("Printing report... Start Page:{0} End Page:{1} Total Page(s):{2}", fromPage, toPage, m_numberOfPages);
                }
                else
                {
                    m_currentPrintingPage = 1;
                    m_lastPrintingPage = m_numberOfPages;
                    Console.WriteLine("Printing report...");
                }
                using (WindowsImpersonationContext wic = WindowsIdentity.Impersonate(IntPtr.Zero))
                {
                    //code to send printdocument to the printer
                   //Set Your Printer Name here
                   printerSettings.PrinterName =  "Microsoft XPS Document Writer";
                   pd.PrinterSettings = printerSettings;
                   pd.PrintPage += this.pd_PrintPage;
                   
                    pd.Print();
                }
            }
            catch (Exception ex)
            {
                //System.Windows.Forms.MessageBox.Show(ex.ToString());
                Console.WriteLine(ex.Message);
            }
            return true;
        }



        /// <summary>
        /// This method renders the report as multidimentional byte array.
        /// </summary>
        private byte[][] RenderReport(string reportPath, ExecutionService.ParameterValue[] parameters)
        {
            // Private variables for rendering
            string historyId = null;
            ExecutionService.ExecutionHeader execHeader = new ExecutionService.ExecutionHeader();
           
            try
            {
                rs.Timeout = 300000;
                rs.ExecutionHeaderValue = execHeader;
                rs.LoadReport(reportPath, historyId);
                if ((parameters != null))
                {
                    rs.SetExecutionParameters(parameters, "en_us");
                }

              
                byte[][] pages = new Byte[0][];
                string format = "IMAGE";
                int numberOfPages = 1;
                byte[] currentPageStream = new byte[1] { 0x00 }; // put a byte to get the loop started
                string extension = null;
                string encoding = null;
                string mimeType = null;
                string[] streamIDs = null;
                ExecutionService.Warning[] warnings = null;
            
                while (currentPageStream.Length > 0)
                {
                    string deviceInfo = String.Format(@"<DeviceInfo><OutputFormat>EMF</OutputFormat><PrintDpiX>200</PrintDpiX><PrintDpiY>200</PrintDpiY>" + "<StartPage>{0}</StartPage></DeviceInfo>", numberOfPages);
                    //rs.Render will render the page defined by deviceInfo's <StartPage>{0}</StartPage> tag
                    currentPageStream = rs.Render(format, deviceInfo, out extension, out encoding, out mimeType, out warnings, out streamIDs);
                   
                    if (currentPageStream.Length == 0 && numberOfPages == 1)
                    {
                        //renderException = EnumRenderException.ZERO_LENGTH_STREAM;
                        break;
                    }

                    //Add the byte stream of current page in pages[] array so that we can have complete report in pages[][] array
                    if (currentPageStream.Length > 0)
                    {
                        Array.Resize(ref pages, pages.Length + 1);
                        pages[pages.Length - 1] = currentPageStream;
                        numberOfPages++;
                    }
                }

                m_numberOfPages = numberOfPages - 1;
               
                return pages;
            }
            catch (System.Web.Services.Protocols.SoapException ex)
            {
                Console.WriteLine(ex.Detail.InnerXml);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            finally
            {
               // Console.WriteLine("Number of pages: {0}", pages.Length);
            }
            return null;
        }



        /// <summary>
        /// Handle the Printing of each page
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="ev"></param>
        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 (System.Threading.Interlocked.Increment(ref m_currentPrintingPage) <= m_lastPrintingPage)
                {
                    ev.HasMorePages = true;
                }
            }
        }



        // Method to draw the current emf memory stream
        private void ReportDrawPage(Graphics g)
        {
            if (m_currentPageStream == null || 0 == m_currentPageStream.Length || m_metafile == null)
            {
                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);
                Point[] points = new Point[3];
                Point destPoint = new Point(0, 0);
                Point destPoint1 = new Point(width/2 , 0);
                Point destPoint2 = new Point(0, height/2);

                points[0] = destPoint;
                points[1] = destPoint1;
                points[2] = destPoint2;
                g.EnumerateMetafile(m_metafile, points, m_delegate);
                // Clean up
                m_delegate = null;
            }
        }



        private bool MoveToPage(int page)
        {
            // Check to make sure that the current page exists in
            // the array list
            if (m_renderedReport[m_currentPrintingPage - 1] == null)
            {
                return false;
            }
            // Set current page stream equal to the rendered page
            m_currentPageStream = new MemoryStream(m_renderedReport[m_currentPrintingPage - 1]);
            // Set its postion to start.
            m_currentPageStream.Position = 0;
            // Initialize the metafile
            if (m_metafile != null)
            {
                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;
        }
}



Call this “PrintReport function” from any event(eg.A button click) from where you want to print the Server Report and pass the “report path as parameter” to the function.
Also you need to pass report’s parameters as parameter to this function…..in my case my report do not have any parameter so I am passing null
In my case my server reportPath is "/EmpInfoReport/EmpDetails"

I will call this function on my Print button click as follows:
private void btnPrint_Click(object sender, EventArgs e)
{
DirectlyPrintReport.PrintManager objPrintManager = new PrintManager();
objPrintManager.PrintReport(@"/EmpInfoReport/EmpDetails", null, -1, -1);
}



You can check following related post as well

Copy SSRS Report on Report Server Programatically

Create a New SSRS report Programmatically


Please let me know is it helpful....

11 comments:

  1. Naren, thanks for posting this. Is this a working example for more than 1 pages of report?

    ReplyDelete
  2. Nice write up. I created an MVC 4 application and added your PrintManager class in.

    Getting following compilation error in VS 2012 with SSRS 2012.

    The type or namespace name 'ReportExecutionService' could not be found

    ReplyDelete
  3. I got the below Error .Please let me know if anything missed


    Error: The type or namespace name 'ReportExecutionService' could not be found (are you missing a using directive or an assembly reference?)

    ReplyDelete
  4. Hello guys, sorry i was away from blogger for long time...
    Here are my comments to your questions

    For this you need to have reporting services installed on your machine along with SQL server.
    And then you need to add reference of those services in your project using Add Service Reference feature in Visual Studio.

    Few more details :
    Found out that there's a difference between adding a Service Reference and a Web Reference. In VS right click on References, Add Service Reference, then at the bottom there's an Advanced button. At the bottom of the next form there's a Add Web Reference button. Use that instead of Service Reference.

    Do let me know if any issues..

    ReplyDelete
  5. I am getting one error The type or namespace name 'ReportExecutionService' could not be found (are you missing a using directive or an assembly reference?)

    ReplyDelete
  6. i am facing one error "client could not found content of "" but expected xml"

    ReplyDelete
  7. Hi Naren,
    Thank you so much for your code and explanation that was very useful.
    When I am running the code everything is fine until the code reaches to Render method then I get an error "Soap Exception was caught" and is saying that:
    "This report requires a default or user-defined value for the report parameter 'T1T2'. To run or subscribe to this report, you must provide a parameter value. ---> "
    My reports needs 6 parameters and the last one is "T1T2" and I've defined all of them but no idea why I get this error.
    Can you please help me to find the reason.

    Thanks,
    Emma

    ReplyDelete
  8. This is really helped me lot..

    ReplyDelete
  9. Thanks Bro !!!!

    I was really looking for this for mass printing and there is not out of box solution available. Works Perfectly fine without any issue. Thanks once again.

    For other persons who are getting "ReportExecutionService" count not be found issue, this is basically your service proxy name set while you were adding your ReportExecutionService Web Service. Please use your respective proxy. Code is tested and printing fine.

    ReplyDelete
  10. My SSRS Web Service URl is "http://axdesk64/ReportServer". I am able to access this url in the web browser, but could not add this url as Web Service Reference in the Visual Studio.
    I am getting the below error:
    The HTML document does not contain Web service discovery information.

    ReplyDelete
  11. Thanks for posting this. There is surprisingly little out there on the subject - even less so for the new SSRS REST API.

    I had the same reference problems for ReportExecutionService. I had to preface each invalid entry with my project namespace and web service reference name Ex. MyProjectNamespace.ExecutionService.ReportExecutionService

    Additionally, I did not have to have SSRS installed locally. Having it on a network machine worked just as well. The print job executes on whatever printer is local to the machine that is executing the above code.

    Again, thanks! This has been immensely helpful!

    ReplyDelete