Introduction
When developing ASP.NET applications that use the Microsoft SQL Server Reporting Services 2005 (SSRS) to display and print reports, you will face a problem when you want to provide a direct print functionality to user outside the traditional Web Report Viewer. This article will describe how to use the RsClientPrint
ActiveX control provided by Microsoft to print SSRS reports inside custom ASP.NET applications.
Note that this article doesn’t include any samples to download; only code snippets and scenario descriptions will be included.
Background
If you Googled through the web regarding printing options for SSRS reports, you will be left with three probable options:
- Render the report to HTML or Images and use JavaScript to print the report using web browser (Internet Explorer or Firefox) printing capability. The odds of this option are:
- When your report is rendered in multiple pages, the pages will not print in correct format. You will have pages that don’t break correctly due the web browser printing margins settings and other printing problems because the browser will print it as an HTML page.
- The web browser headers and footers will be displayed in the printout. You can guide users to remove the web browser headers and footers when using your print functionality. (Users will hate this.)
- Printing landscape reports will require users to change their print settings every time they want to print a report with landscape orientation.
- Export the report to another format that can be used for printing. E.g. PDF or Microsoft Excel. The odds are:
- User must have PDF or Excel viewer to print the reports.
- Print operation will be tedious since the user needs to do many steps to just print the report.
- Use the
RsClientPrint
ActiveX control. This is a good solution to sends the reports directly to the printer. The odds are:
- User must use Internet Explorer as web browser to support ActiveX controls. Also you can write Firefox extension for this task.
- The Microsoft documentation is very short (don’t expect so much information from Microsoft MSDN regarding using
RsClientPrint
. - If you follow the Microsoft documentation, you will face a problem with SSRS authentication as we are going to discuss through this article.
In this article, I will describe how to use the RsClientPrint
in an efficient way and implement the same functionality provided in Microsoft Reports Web Viewer printing.
Introducing RsClientPrint
The RsClientPrint
is an ActiveX control. This control should be registered to clients PCs before we can use it.
The RsClientPrint
control contains several properties and one method. Please refer to Microsoft RsClientPrint
for more information here.
Here is the list of the control properties and methods borrowed from MSDN site above:
RsClientPrint Properties
Property | Type | RW | Default | Description |
MarginLeft | Double | RW | report setting | Gets or sets the left margin. The default value if not set by the developer or specified in the report is 12.2 millimeters. For more information about report margins, see this link. |
MarginRight | Double | RW | report setting | Gets or sets the right margin. The default value if not set by the developer or specified in the report is 12.2 millimeters. For more information about report margins, see, this link. |
MarginTop | Double | RW | report setting | Gets or sets the top margin. The default value if not set by the developer or specified in the report is 12.2 millimeters. For more information about report margins, see, this link. |
MarginBottom | Double | RW | report setting | Gets or sets the bottom margin. The default value if not set by the developer or specified in the report is 12.2 millimeters. For more information about report margins, see this link. |
PageWidth | Double | RW | report setting | Gets or sets the page width. The default value if not set by the developer or the report definition is 215.9 millimeters. |
PageHeight | Double | RW | report setting | Gets or sets the page height. The default value if not set by the developer or the report definition is 279.4 millimeters. |
Culture | Int32 | RW | Browser locale | Specifies the locale identifier (LCID). This value determines the unit of measurement for user input. For example, if a user types 3, the value will be measured in millimeters if the language is French or inches if the language is English (United States). Valid values include: 1028, 1031, 1033, 1036, 1040, 1041, 1042, 2052, 3082. |
UICulture | String | RW | Client culture | Specifies string localization of the dialog box. Text in the Print dialog is localized into these languages: Chinese-Simplified, Chinese Traditional, English, French, German, Italian, Japanese, Korean, and Spanish. Valid values include: 1028, 1031, 1033, 1036, 1040, 1041, 1042, 2052, 3082. |
Authenticate | Boolean | RW | | Specifies whether the control issues a GET command against the report server to initiate a connection for out-of-session printing. |
RsClientPrint Methods
The RsClientPrint
control supports one method only called print. This method is used to launch the print dialog. The method arguments are described in the above Microsoft link. I pasted them here as reference.
Argument | I/O | Type | Description |
ServerPath | In | String | Specifies the report server virtual directory (for example, https://adventure-works/reportserver). |
ReportPathParameters | In | String | Specifies the full name to the report in the report server folder namespace, including parameters. Reports are retrieved through URL access. For example: "/AdventureWorks Sample Reports/Employee Sales Summary&EmpID=1234" |
ReportName | In | String | The short name of the report (in the example above, the short name is Employee Sales Summary). It appears in the Print dialog box and in the print queue. |
How RsClientPrint Works
The RsClientPrint
renders reports in EMF (Enhanced Meta File) format to send the reports to the printer. The EMF format is a vector format which makes the rendered report independent from the resolution. Visit this Wikipedia link for more information.
The RsClientPrint
uses the Reporting Service direct URL access to render the report to EMF format. Direct URL access enables users to communicate with the Reporting Service using Reporting Service URL and query strings to pass commands to the Reporting Service. Visit this MSDN link for more information about URL access.
When rendering the reports using the EMF format, the RsClientPrint
instructs the Reporting Service to persist the rendered report stream using the URL command rs:PresistStream=true
. This command will force Reporting Service to render the first page and return the result stream to the caller directly. To get the next page, RsClientPrint
will replace the rs:PresistStream
command with rs:GetNextStream=true
. The Reporting Service will render the next page and return its stream to the caller. When there are no more pages, Reporting Service will return an empty stream.
RsClientPrint
will read the result EMF streams and send it to the selected client printer or prepare it for preview.
Microsoft Web Report Viewer
When using the Report Viewer inside Internet Explorer to render SSRS reports, you will have a little print button. Pressing the print button will show up a print dialog and a preview capability to review the report before printing. This print dialog is Microsoft ActiveX control called RsClientPrint
.
What is interesting about the Report Viewer is that if you reviewed the page source from Internet Explorer, you will not find any code that references the RsClientPrint
.
How does the Web Report Viewer print?
The Report Viewer uses IFRAM
object to load the print code when you press the print button. This IFRAME
object has a style with display: none. By this way the IFRAM
will never be visible in the client browser.
The first time you render the report in the Report Viewer, the IFRAM
src attribute is empty. But when printing the report, the src attribute will point to another temp page that contains the required HTML code to print the required report.
Below is a code snippet of a sample HTML code that is generated by the Report Viewer and used by the hidden print IFRAME
.
<HTML>
<BODY onload="Print()">
<OBJECT ID="RSClientPrint" CLASSID="CLSID:FA91DF8D-53AB-455D-AB20-F2F023E498D3"
CODEBASE="/Reports/Reserved.ReportViewerWebControl.axd?
ReportSession=4nfhfoj4e20k4v55jrw3ijnr&ControlID=
aa4dfda2cb2b48388a2956ca7db7d995&Culture=1033&
UICulture=9&ReportStack=1&OpType=PrintCab#Version=2005,090,3042,00"
VIEWASTEXT></OBJECT>
<script language="javascript">
function Print()
{
if (typeof RSClientPrint.Print == "undefined")
{
alert("Unable to load client print control.");
return;
}
RSClientPrint.MarginLeft = 9.906;
RSClientPrint.MarginTop = 9.906;
RSClientPrint.MarginRight = 9.906;
RSClientPrint.MarginBottom = 9.906;
RSClientPrint.PageHeight = 296.926;
RSClientPrint.PageWidth = 230.124;
RSClientPrint.Culture = 1033;
RSClientPrint.UICulture = 9;
RSClientPrint.UseEmfPlus = true;
RSClientPrint.Print("/Reports/Reserved.ReportViewerWebControl.axd",
"ReportSession=4nfhfoj4e20k4v55jrw3ijnr&ControlID=aa4dfda2cb2b48388a2956ca7db7d995&
Culture=1033&UICulture=9&ReportStack=1&OpType=PrintRequest","Cashier
Balance")
}
</script>
</BODY>
</HTML>
From the HTML code above, this is a complete HTML page tags that includes the following:
- HTML page tags (HTML and Body tags)
RsClientPrint
object declaration - A JavaScript print method
Body
attribute to call the print
method when the page loads
What is interesting here is that if we look to the parameters passed to the RsClientPrint Print
method, we will not find a URL of the Remote Reporting Service? We will find a URL of the Report Viewer HTTP Handler. This HTTP Handler is registered automatically in the web application web.config file when the Report Viewer is dropped in the ASP.NET web form.
Why is the Report Viewer using this HTTP handler instead of the Report Server URL? The answer is to avoid authentication when using the RsClientPrint
to print the reports.
To understand the reason, we need to know more about the authentication mode of the Reporting Service as follows:
The Reporting Service is hosted by default under the IIS and uses the Windows Integrated Security to secure connection to the Reporting Service web services. The windows integrated security will provide the Reporting Services information about the caller and authorize him for privileged items only.
Also the Reporting Service uses integrated windows accounts to specify access privileges to different Reporting Service folders, reports and data sources definitions.
So when the user tries to connect the Reporting Service in default configuration, the IIS will pass the user window account to the Reporting Service which in turn will use it to authorize the user to interact with Reporting Service items according to his assigned privileges.
This default configuration will not work if your custom web application is published in internet or you can’t privilege every user for the Reporting Service items.
The simple solution to authorize every user is to do the following:
- Add the user which ASP.NET application runs under IIS to the Reporting Service authorized users list and assign the required privileges to this user. You can use the SSRS Reports Manager to do this.
- Assign the required privileges to this user. You can use the SSRS Reports Manager to do this.
For IIS 5.1, this will be the ASPNET user. Some developers prefer to personalize the ASP.NET application using custom windows user. In this case, they add this user to the Reporting Service instead of the ASPNET default user.
For IIS 6.0 and 7.0, this will be the identity at which the IIS application pool uses to run under the IIS. I can’t go further about the application pools here. You can read more about the application pools in the IIS site.
Doing this will enable the ASP.NET application to use the Report Viewer control to render reports to user using the application process identity to authorize the requests to the Reporting Service.
The above long rational explains why the Report Viewer control uses the HTTP handler to print the report.
If the Report Viewer passes the Reporting Service URL instead of using HTTP handler, the user will need to enter Windows account user name and password to gain access to the Reporting Service. It needs to do so because it is printing outside of the ASP.NET application context.
The above will introduce problems if:
- The application is enabled over the internet and there is no Windows accounts defined for users (Forms authentication)
- The user is not authorized in the Reporting Service and expecting a print functionality when using the ASP.NET web application.
So Report Viewer directs the RsClientPrint
to communicate with its HTTP handler to accept its rendering requests. The HTTP handler will communicate to the Reporting Service with direct URL access using the ASP.NET application credential context. The Reporting Service will accept the connection from the application since it is running with authorized user identity. The Reporting Service will render the requested reports and returns the EMF stream to the application. In turn, the application will forward these streams to the RsClientPrint
control.
Note: The above information is derived by observation; I didn’t find any formal reference.
Implementing the RsClientPrint Handler
As we see from the above description, if we have an HTTP handler that accepts the RsClientPrint
requests and render the required reports in the context of the current ASP.NET application, we will be able to print the reports without any need for authentication with the Reporting Service.
The solution can be resolved by an HTTP handler or a simple ASP.NET web form. I thought since this article is long enough I will use the web form instead of HTTP handlers for the sake of simplicity.
Let us begin by creating a web form called PrintReport.aspx. This web form will accept requests from the RsClientPrint
control and send back the rendered EMF streams.
The following is the PrintReport.aspx code:
protected void Page_Load()
{
if(Request.QueryStrings.Count > 0)
{
string requestUri = string.Format("http://{0}/reportserver?{1}",_serverName,
Request.QueryStrings.ToString());
WebRequest request = WebRequest.Create(requestUri);
CookieContainer cookies = null;
if(Session["PrintReport.CookieContainer"] != null)
{
cookies = Session["PrintReport.CookieContainer"] as CookieContainer;
}
else
{
cookies = new CookieContainer;
Session["PrintReport.CookieContainer"] = cookies;
}
(request as HttpWebRequest).CookieContainer = cookies;
request.UseDefaultCredentials = true;
try
{
using (WebResponse rsResponse = request.GetResponse())
{
Stream s = rsResponse.GetResponseStream();
Response.Clear();
Response.BufferOutput = true;
BinaryReader reader = new BinaryReader(s);
byte[] buf = new byte[256];
int count = 0;
do
{
count = reader.Read(buf,0,256);
if(count > 0)
{
Response.OutputStream.Write(buf,0,count);
}
else
{
Response.End();
return;
}
}while(count > 0);
Response.Flust();
Response.End();
}
}
catch
{
}
}
In the above code, the RsClientPrint
control sends requests to the PrintReport.aspx page. The page will process the request by forwarding it to the reporting server using the current ASP.NET credentials. When the response is received from the reporting server, the page will forward this response to the RsClientPrint
control.
The RsClientPrint
will continue sending requests to the PrintReport.aspx page until it receives an empty response. The empty response is received only when there are no more report pages to render in the current reporting server session.
The reporting server identifies the session using the request cookies container in the incoming request.
Embedding the IFRAME Object
In the page you want to print, you can embed the IFRAM
object by inserting the following snippet in your web form:
<asp:Literal ID="IFrameLiteral" runat="server" Text=""/>
The src attribute of this IFRAME
object points to the page that contains the required code to launch the RsClientPrint
control Print
method.
The trick here is how to generate this dynamic code?
We can use the PrintReport.aspx web form to generate this dynamic code as follows:
- Place a formatted
string
in a text file. Name this file PrintScript.txt. - Paste the following text to the file:
<HTML>
<BODY onload="Print()">
<OBJECT ID="RSClientPrint"
CLASSID="CLSID:FA91DF8D-53AB-455D-AB20-F2F023E498D3" CODEBASE="RsClientPrint.cab"
VIEWASTEXT></OBJECT>
<script language="javascript">
function Print()
[
if (typeof RSClientPrint.Print == "undefined")
[
alert("Unable to load client print control.");
return;
]
RSClientPrint.MarginLeft = {0};
RSClientPrint.MarginTop = {1};
RSClientPrint.MarginRight = {2};
RSClientPrint.MarginBottom = {3};
RSClientPrint.PageHeight = {4};
RSClientPrint.PageWidth = {5};
RSClientPrint.Culture = 1033;
RSClientPrint.UICulture = 9;
RSClientPrint.UseEmfPlus = true;
RSClientPrint.Print("{6}", "{7}", "{8}")
]
</script>
</BODY>
</HTML>
- When user wants to print a report, the text in the PrintScript.txt can be formatted as follows:
string frameSrc = File.Load(Request.GetApplicationPath() + @"PrintScript.txt");
frameSrc = string.Format(frameSrc,marginLeft,marginTop,marginRight,
marginBottom,"/PrintReport.aspx",Report
Path and parameters ,"Report Name");
frameSrc = frameSrc.Replace(‘[‘,’{‘);
frameSrc = frameSrc.Replace(‘]‘,’}‘);
- Store the created text in a session variable:
Session["FrameSource"] = frameSrc;
IFrameLiteral.Text = "<IFRAME
id="PrintIFrame" src="PrintReport.aspx"/>
- When loading the page, the
IFRAM
will load the PrintReport.aspx page. The PrinReport.aspx page will intercept the call and send the response as illustrated in the following code:
protected void Page_Load()
{
if(Request.QueryStrings.Count == 0)
{
Response.Clear();
if(Session["FrameSource"] != null)
{
Response.Write(Session["FrameSource"].ToString());
}
Response.End();
}
}
From the above code, the response will return the content of the FrameSource
session variable to the IFRAME
. This will contain a full HTML code to call the RsClientPrint
object print method.
- The following is a sample of the resulted
IFRAME
HTML code:
<HTML>
<BODY onload="Print()">
<OBJECT ID="RSClientPrint"
CLASSID="CLSID:FA91DF8D-53AB-455D-AB20-F2F023E498D3" CODEBASE="RsClientPrint.cab"
VIEWASTEXT></OBJECT>
<script language="javascript">
function Print()
{
if (typeof
RSClientPrint.Print == "undefined")
{
alert("Unable to load client print control.");
return;
}
RSClientPrint.MarginLeft= 10;
RSClientPrint.MarginTop= 10;
RSClientPrint.MarginRight= 10;
RSClientPrint.MarginBottom= 10;
RSClientPrint.PageHeight= 297;
RSClientPrint.PageWidth= 210;
RSClientPrint.Culture= 1033;
RSClientPrint.UICulture= 9;
RSClientPrint.UseEmfPlus= true;
RSClientPrint.Print(http:
"/Reports/FinancialReport&Year=2008", "Financial Report")
}
</script>
</BODY>
</HTML>
- The
RsClientPrint
control will communicate with the print page as shown above and the RsClientPrint
control print dialog will appear.
So the scenario is as follows:
- User clicks ASP.NET print button.
- The print button
OnClick
event will be fired. - In the
OnClick
event handler, user build the IFRAME
source string
and store it in a session variable. - The
IFRAM
tries to load the PrintReport.aspx page. - The PrintReport.aspx page sends the appropriate response.
- The
RsClientPrint
control declared in the IFRAM
JavaScript loads the Print Dialog. - The
RsClientPrint
control communicates with the PrintReport.aspx to get streams of rendered report. RsClientPrint
control prints the received EMFs.
History
This is version 3 of the article and I will update this article frequently to add the missing code and enhance its content.