Practical ASP.NET
Build Printable ASP.NET Pages
Provide printing capabilities in your ASP.NET apps using server-side controls and JavaScript automation, or by using VS.NET add-in tools such as Crystal Reports.
Technology Toolbox: C#, ASP.NET, Visual Studio .NET 2003, JavaScript
Users should be able to print order confirmations, invoices, and the like directly from your Web application. And no, the old Shift + Print Screen approach from the 3270 mainframe emulation screen days won't cut it, for the same reason that the JavaScript window.print() command often won't, even though it triggers the operating system's Print dialog with minimal code:
<A href="javascript:window.print()">
Print Me!</A>
This gives users that invoice they wantplus a lot more that they don't, including application navigation controls, superfluous graphics, command buttons, and Web site headers/footers. Also, JavaScript is vulnerable to hacks and viruses, so many users turn off script support in their browsers. This nullifies even the most elegant JavaScript solution unless you can be sure the target audience has it enabled.
The safest way to provide effective printing capabilities is to render a "printable page" that users can print using their browser's Print command. You pre-format a printable page to contain only user-relevant information, the code renders the page, users initiate the browser's Print command, and the operating system brings up its Print dialog. You can automate this process in JavaScript if your users' browsers support that.
You can also use frames. Many Web sites provide specific areas for menu/navigation, site headers, and content using frames, though this method seems to be fading compared to using DIV and IFRAME areas. Still, you might be able to use a frame as a printable page if you've set up your Web app to use frames and you put all your result content into a frame. Have users print the Content frame from within the browser by right-clicking within the frame and selecting Print. This works if you're using current versions of IE or Netscape (again, you could automate this process in JavaScript).
Suppose you have a page with a frameset containing two frames, "Menu" and "Content":
<frameset cols="20%,*" border="0">
<frame name="Menu"
src="OrderMenu.aspx">
<frame name="Content"
src="SimplePrintingWithFrames.aspx">
</frameset>
Add the code for printing the Content frame from a button:
<INPUT type="button" value="Print Content"
OnClick="javascript:PrintContent()">
<script language="JavaScript"><!--
function PrintContent() {
parent.Content.focus();
// Required to support IE
parent.Content.print();
}
//--></script>
However, you don't need frame support to create a printable duplicate of your content area in a separate page. Consider an order lookup screen based on the Northwind database, with a dropdown listbox to select the order ID and an area to display the order and line-item detail information. A straightforward little program can select the order and fill the order details area (see Listing A).
You want to let users select the order ID, which then populates the content area (the order and order detail information). Create a printable page that takes this order ID and displays the static information sans objects, buttons, and post backs. Copy the original ASPX file and paste it within the Solution Explorer. This creates the file copy of <Original Filename>. Rename the file and all the class name references to this new name (by default, VS.NET creates a Page class that matches the filename). Now delete all the unnecessary information and use the OrderID session variable to retrieve the specific information for the desired order ID.
Finally, create a link from the original order page to this printable version. Use a LinkButton control with the text "Printable Version" so users know another window will pop up containing a printable page. The control's event handler holds the value for the current order ID, stored in a session variable (OrderID) for the print page to use. Add some JavaScript to open a new window using the window.open() command:
Response.Write(strRedirectScript);
This appends your script block within the document's <BODY> tag on the rendered page.
Standardize Printing Capabilities
Once you have the printable page built, you can perform a Server.Transfer or Response.Redirect and target the results page, but this takes users to the printable page in their current browser window. (Server.Transfer is faster, but works only on pages on the same server.) Then you need to provide some mechanism to let users get back, apart from making them hit the Back button (see Listing 1).
This approach forces you to create a print page for any area of your application that might warrant user printability, but it is a standard, safe method for providing printing capabilities. You can get the same result with fewer pages by creating a more generic printing ASP.NET page to handle all print requestsif you standardize the way you lay out all your pages.
MSDN provides a Print button for all its articles; when you click on it, nothing pops up but the operating system's Print dialog box. This is a good idea, so build similar functionality into your order page. Add a LinkButton to your order page; call it "Generic Print." Surround the order page results area with a DIV tag:
<DIV id="PrintContent" runat="server">
[ASP.NET Code for Results Goes here ?]
</DIV>
Now create a hidden IFRAME called HiddenPrintFrame at the end of the page. Set the runat="server" property for the IFRAME and declare the object as type HtmlGenericControl in your code-behind file so you can access its properties at run time. Use this IFRAME to call your generic printing page dynamically when users click on the button. Otherwise, it stays hidden, so users see only the Print dialog:
<iframe id="HiddenPrintFrame" width="0"
height="0" runat="server"></iframe>
Next, create a print page template, PrinterPageTemplate.htm. Add formatting for headers and footers, then create a DIV area called Content, responsible for holding the printable content from your order page:
<div id="Content">Placeholder</div>
Lastly, build an external JavaScript function file to hold your printing function (see Listing 2). This keeps your JavaScript code from being rendered in the page. Everything on the order page works as before, except you've placed all the controls containing the retrieved order data between the special DIV PrintContent. When users click on the Generic Print button, your code sets the SRC attribute of the IFRAME HiddenPrintFrame to your generic printing page:
HiddenPrintFrame.Attributes["src"] =
"PrinterPageTemplate.htm";
The IFRAME is hidden, so users don't notice anything. When this page loads in the IFRAME, it runs the JavaScript routine to copy the information from the PrintContent DIV to the Content DIV on the template page. The focus sets to the hidden IFRAME (required to print an IFRAME within IE), and the frame prints. Only the Print dialog comes up when users click on the Print button, and users receive only the order information on the screen (complete with standardized headers and footers).
Sometimes your content area spans more than one printed page, so consider placing printer page breaks in key areas to make the printed pages look better. Force page breaks using the <P> tag's Style attribute (supported only by browsers such as IE 4.x and current versions of Mozilla that support the Cascading Style Sheet version 2 specification):
<p style="page-break-before: always">
You can gain better control over page headers and footers by hooking into the Render method for the page. Count output lines, then insert the appropriate headers, footers, and page breaks.
Print With Crystal Reports
Crystal Reports .NETincluded with VS.NETlets you take page printing to the next level. For example, you can use it to let users display or save your printable pages in many formats, including Adobe Acrobat Reader, Word, Excel, and CSD. You're free to create and deploy reports with up to five concurrent connections.
Start working on your Crystal Reports-enabled printable pages by creating a LinkButton called CrystalPrint. Use the same tactic as before to bring up another window programmatically when users click on this button. The window's source is PrintingWithCrystal.aspx.
I've developed a stored procedure, GetOrderDetailsForOrder, to retrieve joined table results with order details matching the @MatchOrderID parameter (create this stored procedure by running DBSCRIPT.SQL from the source download). Start using the drag-and-drop method for creating data adapters and typed data sets to expedite creating ADO.NET query code, so you also must change a line of the IDE-generated code in order to access this parameter programmatically:
ParamOrderID =
this.sqlSelectCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter(
"@MatchOrderID", System.Data.SqlDbType.Int, 4));
VS.NET rewrites this code if you manipulate any data objects on the Design panel, losing the setting of ParamOrderID, which Page_Load required. So be careful. Also, remember to change the name of the SQL Server in the SQLConnection object's connection string (my SQL Server's name is DOMINION):
this.sqlConnection1.ConnectionString =
"workstation id=DOMINION;packet size=4096;" +
" user id=sa;integrated security=SSPI;data" +
" source=localhost;persist security
info=False;initial catalog=Northwind";
Now create a Crystal Reports RPT file, which can do all the work or simply act as a formatting template. You can control your own queries by creating a data set based on the GetOrderDetailsForOrder stored procedure, then generate a report based on that typed data set (see Figure 1).
Start the Crystal Reports wizard automatically by going to File | Add New Item | Crystal Report. The wizard adds a class module to your project with the same name as the RPT file. You'll use this class later to create the report dynamically and view it inside a Web page.
Build a Web page that holds a viewer for the report. Users view reports through the CrystalReportViewer object. I choose to query my own data rather than encapsulate it inside the RPT file to avoid forcing users to enter the order ID or database credentials. You need to set some properties in the Page_Load event programmatically (see Listing 3).
Retrieve the data for your data set, then create an instance of a Crystal Reports ReportDocumenta container for the actual RPT file. Use the class generated when you created the Crystal Report:
ReportDocument myReportDocument =
new ReportOrders();
Set the data source for myReportDocument as that data set, then set the ReportSource property of the Viewer control within the ASP.NET page to this ReportDocument:
protected CrystalDecisions.Web.
CrystalReportViewer
CrystalReportOrders;
myReportDocument.SetDataSource
(dsReportOrderDetails);
CrystalReportOrders.ReportSource =
myReportDocument;
This code turns off paging, toolbars, and a treeview, because this report deals with a single order (see Figure 2). You can enable these properties to create sophisticated reporting screens with built-in paging, drill-down, and zoom capabilities. The Crystal Reports viewer also checks the browser's capabilities before rendering and adapts the rendered display accordingly. Imagine adding that kind of customization code for portable devices, phones, and down-level browsers.
Print PDFs Instead of HTML
Create another button on your order page and link it to a different ASP.NET page that renders an Adobe Acrobat PDF file instead of HTML. Users can print or save a local copy using Acrobat Reader. Call the code-behind file for this new ASP.NET page OrdersPDFExport.aspx (see Listing 4). The Page_Load event renders all the information dynamically, so the Web page holds no objects.
Use a session variable to cache the order ID, then query the results into a data set, as in the last example. However, this time don't use the CrystalReportsViewer object. Instead, create the ReportDocument and set its data source. Then create a temporary file location within the virtual Web to hold the exported PDF document, using the session ID as a unique indicator:
strTempFileLocation =
Session.SessionID.ToString() + ".pdf";
Next, create two options objects. DiskFileDestinationOptions tells the Crystal Reports engine where to create the disk file. Retrieve a physical path to your app's virtual directory using the Server.MapPath method. The ExportOptions object holds the information on how to format the export. Set properties that export a PDF file (you can change these settings easily to export a Word or Excel document):
myDiskFileOptions = new
DiskFileDestinationOptions();
myDiskFileOptions.DiskFileName =
Server.MapPath(strTempFileLocation);
myExportOptions =
myReportDocument.ExportOptions;
myExportOptions.DestinationOptions =
myDiskFileOptions;
myExportOptions.ExportDestinationType =
ExportDestinationType.DiskFile;
myExportOptions.ExportFormatType =
ExportFormatType.PortableDocFormat;
Create the export file using the Export() method of the ReportDocument object. Then feed it back to the browser, but remember that ASP.NET has probably done some rendering alreadyeven though the ASPX page is empty, basically. Clear out the Response stream, write the PDF file to the Response stream, and flush out the stream to the client:
Response.ClearContent();
Response.ClearHeaders();
Response.ContentType = "application/pdf";
Response.WriteFile(Server.MapPath
(strTempFileLocation));
Response.Flush();
Response.Close();
Clean up after yourself by calling the System.IO.File.Delete() method at the end of the Page_Load to delete that temporary PDF file. The result is a dynamically created PDF file users can view, print, or save.
Now you have a variety of printable page options for your toolbox, ranging from quick and dirty to slick as commercial software. Try them out for yourself and I'm sure that one of them will soon become your favorite.
About the Author
Doug Thews is the director of software development for D&D Consulting Services in Dallas. Doug has more than 19 years of software-development experience in C/C++, VB/ASP, and VB.NET/C#. He writes the Getting Started column for Visual Studio Magazine, and coauthored the book, Professional ASP.NET Performance (Wrox/Wiley Press). Reach Doug at [email protected].