Crystal Reports: Add Professional Quality Reports to Your Application with Visual Studio .NET

MSDN Magazine

Crystal Reports

Add Professional Quality Reports to Your Application with Visual Studio .NET

Andrew Brust
This article assumes you're familiar with Visual Studio .NET
Level of Difficulty    1   2   3 
Download the code for this article: Crystal.exe (391KB)
SUMMARY Crystal Reports for Visual Studio .NET provides a comprehensive reporting solution for .NET developers that is thoroughly integrated with both the Visual Studio .NET IDE and the .NET Framework. Crystal Reports supports ADO.NET, XML Web Services, and ASP.NET server controls and caching. It also integrates seamlessly with the Visual Studio .NET Server Explorer, toolbox, and design environment. It has a rich programming model and flexible options for customizing and deploying reports. These major features and others covered here take the drudge work out of data representation in your own applications.

Developers using Microsoft® technologies have had a long and close relationship with Crystal Reports, dating all the way back to the introduction of Visual Basic® 1.0. Now with the advent of the .NET platform and Visual Studio® .NET, Crystal Reports has once again set the standard for third-party integration with Microsoft tools. Crystal Reports for Visual Studio .NET was developed in coordination with Microsoft engineers to integrate cleanly with the .NET Framework and Visual Studio .NET. It features an integrated designer for creating new reports, the ability to import any existing Crystal Reports (or Visual Basic 6.0 data reports) to .rpt format, and many other features that I'll describe in depth in this article.
      Here, I'll step through the creation of a Windows® Forms application, add an ASP.NET Web Service, and build an ASP.NET client application. Along the way you'll see some useful coding tricks for tasks such as generating Acrobat (PDF) documents from your reports, building reports on parameterized SQL Server™ stored procedures, and passing reports using the current user's database security credentials. Downloadable sample code for this article includes Windows Forms, ASP.NET Web Forms, and ASP.NET Web Services applications.

A Simple Report

      To begin with something simple, I will create a report from the Customers table of the SQL Server Northwind database and view it in a Windows Forms application. I'll start by creating a new application for Windows in Visual Studio .NET. (I use Visual Basic, but the concepts are portable to all .NET languages.) Once the application is created, right-click the project name in the Solution Explorer, select Add | Add New Item from the context menus. Then in the Add New Item dialog box, select Crystal Report from the Templates list, name the report CustomersBasic.rpt, and click Open (see Figure 1).

Figure 1 Creating a Simple Report
Figure 1 Creating a Simple Report

      When the Crystal Report Gallery dialog opens, you can accept the default settings (Using the Report Expert and Standard), and click OK. When the Report Expert opens, drill down on the OLE DB (ADO) branch of the treeview control in the Data tab. When the OLE DB (ADO) dialog opens, select the Microsoft OLE DB Provider for SQL Server, specify the server and login information for your SQL Server, select the Northwind database, then click Finish. Back in the Standard Report Expert, drill down through the Northwind node in the treeview to the Tables node, then double-click the Customers node. In the Fields tab, click the Add All button to add all the fields from the customers table to the report. Next, make sure you assign the report a title and style in the Style tab and click the Finish button.
      The Crystal Reports for Visual Studio .NET designer is now activated and your report is displayed. Notice that the Field Explorer window now appears and is docked to the Toolbox and Server Explorer windows. Within the Toolbox, a Crystal Reports palette is available. Also, a Crystal Reports toolbar now appears at the top left of the Visual Studio .NET toolbar area.
      I will use this report in various ways as I go through each sample in the article. My first sample involves simply displaying this report, unmodified, in a window. This is easy. Just drop a CrystalReportViewer from the Windows Forms palette of the Toolbox onto the designer for the default form in the project, set its name property to cvwMain, its Dock property to Fill, its ReportSource property to my report (using the Browse option), and then run the application. If I designed the report using a database log in with a blank password, the report will display immediately; otherwise, I am required to log in before displaying. I'll show you how to write code shortly that automates this log in, so that already authenticated users do not have to log in a second time just to run reports.

Strongly Typed Reports

      Now that I have covered the basics, let's move on to more sophisticated features. Just as it's possible to create strongly typed DataSets with their own properties and methods, it is also possible to create strongly typed reports. Actually, all reports created with Crystal Reports and Visual Studio .NET result in a strongly typed report class as a by-product of the design process itself. To see this, click the Show All Files button in the Solution Explorer, drill down on CustomersBasic.rpt, and you'll notice that a class file has been created by Visual Studio .NET. I can create an instance of this class in code and assign it to the ReportSource property of the CrystalReportViewer object on my form. To do so, I add the following line of code to the Load event of the form. (I reset cvwMain's ReportSource property to None in the Properties Window first.)

  cvwMain.ReportSource = New CustomersBasic()


      But just as the DataSet component designer can be used with strongly typed DataSets, strongly typed reports also offer a component designer. If you click on the Components palette of the Toolbox window in Visual Studio .NET, you'll see an object called ReportDocument. If you drag one onto your form, you'll see the class name of your report listed in the dropdown portion of the Name combobox in the Choose a ReportDocument dialog. Select it and click OK, and you'll have a runtime-available instance of your report class. Rename the object to cbsMain (cbs is the prefix for CustomersBasic), and substitute the following line of code in the form's Load event:

  cvwMain.ReportSource = cbsMain


Now run your application again and you'll see that the report runs as before. The difference in these last two runs is that, rather than referencing a specific file in a particular physical location, your ReportViewer references an object that could be instanced from a class in the application's assembly, an external assembly, or, as you will see later, even in an XML Web Service.
      As I mentioned, if you supplied connection information that included a user ID with a blank password when you created the report, the report will open right away. In other cases, Crystal Reports for Visual Studio .NET will prompt you to log in. That's because all of the connection information except the password is saved with the report. If you'd like to supply a password at run time, or a different set of connection information than was supplied when the report was designed (this is very often the case), use the code in Figure 2 before setting cvwMain.ReportSource.
      In Figure 2, I arbitrarily set the server, user ID, and password to localhost, ReportUser, and msdn, respectively. In most applications, you'd probably want to use global variables or properties of a class, or a perhaps Session variables in the case of an ASP.NET application, to supply the database name and the credentials the user of your application supplied during her original login. Notice the For Each loop in the code. My report has only one table, making the loop somewhat unnecessary (I could have just referenced cbsMain.Database.Tables(0)), but this code will work with any Crystal Reports for Visual Studio .NET report, and I prefer to maintain this flexibility.

The Server Explorer

      Now let's look at other ways to configure a CrystalReportViewer control. Another design-time option is to use the Server Explorer. Because of a late change to the .NET Framework, this feature of Crystal Reports for Visual Studio .NET is not correctly enabled by the Visual Studio installer. To fix this, and before continuing with the steps in this article, make sure to set the permissions on the Crystal Reports for Visual Studio .NET folder to full control for the user ASPNET. (The default folder location, by the way, is C:\Program Files\Microsoft Visual Studio .NET\Crystal Reports). Do this in the Security tab of the folder's property sheet. For the Security tab to appear, Windows XP users may first need to select the Tools | Folder Options menu item in an Explorer window, click on the View tab, and clear the "Use simple file sharing" option, which is the last item in the Advanced settings list. Once these steps are completed, the following steps should work correctly.
      Under the Servers node, click through the node labeled with the name of your local machine, then the Crystal Services node beneath it, and finally the Server Files node. You will see that two folder nodes appear beneath this Server Files node, and underneath each of those appear several report files (see Figure 3). Each of these .rpt files may be dragged onto the form's designer, which will result in the creation of a ServerFileReport component designer object that points to the correct .rpt file. Interfacing this facility with your own report is the next step.

Figure 3 Server Explorer
Figure 3 Server Explorer

      It turns out that the report folders and files displayed in this section of the Server Explorer are all located within a particular directory on the server's hard drive. By default, this folder is located in the Samples\Reports subdirectory of the default Crystal Reports folder mentioned earlier. The two folders mentioned previously are subdirectories located here and the individual .rpt files are physically contained within those folders.
      To make your report appear in Server Explorer, copy it to the parent folder, one of the child folders, or your own folder created under the parent. For example, you can create a folder called MSDN in the Samples\Reports folder, and copy CustomersBasic.rpt there. Then if you right-click the Server Files node under Crystal Services in the Server Explorer and select Refresh from the context menu, the MSDN® folder should appear, and under it a node for CustomersBasic.rpt. Drag it onto the form, and rename the resulting object sfrCustomersBasic. In the Properties window, set cvwMain's ReportSource property to point to this object. Comment out any code in the form's Load event, run the application, and you'll see the report display perfectly.

Reporting from DataSets

      I will talk about more advanced features of the ServerFileReport component designer later on. For now, the next step will be to modify the report to get its data from an ADO.NET DataSet instead of a physical database via an OLE DB provider. First, let's create a strongly typed DataSet based on the Customers table in Northwind. The easiest way to do this is to add a new empty DataSet to the project and name this DataSet dsNorthwind.xsd through the Add New Item dialog (see Figure 4).

Figure 4 Adding a DataSet
Figure 4 Adding a DataSet

      Next, open the DataSet's designer by double-clicking on its node in the Solution Explorer. Then from the Server Explorer, drag the Customers table onto the design surface of the DataSet and save the file. Next, open the report and right-click on its design surface to get a context menu, then select Database and Set Location. In the Set Location dialog, expand the Project Data node, the ADO.NET DataSets node, the node for your DataSet, and select the Customers table. Finally, from the Current Data Source combobox, select Customers and then click the Replace button. Your report will now look to your strongly typed DataSet for its metadata and will run when bound to an instance of it. Click the Close button to proceed.
      Now that I have modified the report to run against an instance of dsNorthwind, I need to create one in my form and set the CrystalReportViewer's ReportSource to it. To do this, drag the Customers table from the Server Explorer onto the designer surface of the form. This will add SQLConnection and SQLDataAdapter objects to the form that points to the Northwind database and its Customers table, respectively. Rename these objects scnNorthwind and sdaCustomers, modify the ConnectionString property of scnNorthwind to include a valid user ID and password, then right-click sdaCustomers and select Generate Dataset. In the "Choose a dataset" section, select the Existing radio button, and the class name of your strongly typed DataSet from the adjacent combobox (these selections will most likely be the default settings anyway). Figure 5 illustrates this clearly.

Figure 5 Generate DataSet
Figure 5 Generate DataSet

Click the OK button, rename the new DataSet object "ndsMain," and change the Load event code to the following snippet:

cvwMain.ReportSource = cbsMain


Notice that this code first populates ndsMain, an instance of the dsNorthwind strongly typed DataSet class I created. Next, the code sets the DataSource property of cbsMain, the instance of the strongly typed report class CustomersBasic, to ndsMain. Finally, as in previous examples, I point the ReportSource of cvwMain, my CrystalReportViewer, to cbsMain.
      Start the application and you'll see the report runs and appears exactly as it did before. The difference in this example is that the data comes not directly from a database but from a disconnected DataSet object. In my case, the DataSet was populated by a straightforward select query against the Customers table. (Look at the CommandText property of sdaCustomer's SelectCommand object property to see the actual query text.) I could have built this DataSet from whatever data source or sources I might have chosen, as long as the constructed dataset complied with ndsNorthwind's structure.

Reporting from Stored Procedures

      To illustrate this last point, let's create a parameterized stored procedure to run against the Customers table called spCustomers that takes a string parameter to be used in a WHERE clause to limit the number of records returned. Then in code, I can build a DataSet against that stored procedure and bind the report to this dataset. This is the stored procedure code.

  CREATE PROCEDURE dbo.spCustomers
@CustPattern nVarChar(40)
select * from Customers Where CompanyName Like @CustPattern + '%'


      You can create this stored procedure through any means you're comfortable with; doing so through Visual Studio .NET is quite convenient, as shown in Figure 6. In order to report from this stored procedure (using the arbitrary value of "A" for @CustPattern), I modify the form's Load event code, as you can see from the example shown in Figure 7.
      In this code, I am using none of the objects I created at design time except scnNorthwind. Most significantly, I am not using an instance of my strongly typed DataSet, dsNorthwind, as the report's data source, but am instead using an untyped ADO.NET DataSet object called dsReport. This is extremely important—even though I have designed the report against a strongly typed DataSet, at run time I set its data source to a generic, untyped DataSet object whose structure merely matches that of dsNorthwind. When I call the Fill method of the DataAdapter object, I do so using the overloaded version that accepts a table name, and I supply Customers as that table name to ensure that dsReport matches dsNorthwind structurally. If I didn't, the report would have come up without any data.
      It is also possible to design a report directly against a stored procedure much as my first example was designed directly against a table. Setting the parameter's value in code requires some command of the objects, properties, and methods in the various Crystal Reports for Visual Studio .NET namespaces. Let's take a look.
      First, you need to configure Crystal Reports for Visual Studio .NET to make stored procedures selectable as data sources for reports. To do this, right-click on a blank area of the report's designer surface and choose Designer | Default Settings from the context menus. In the Default Settings dialog, click on the Database tab, make sure the Stored Procedures checkbox in the Show section is checked, then click OK.
      Next, right-click again on the designer surface and choose Database | Set Location from the context menus. In the Replace With treeview, drill down on the OLE DB (ADO) node and once again, in the OLE DB (ADO) dialog, supply all the information necessary to connect to your copy of the SQL Server Northwind database. Next, back in the Set Location dialog, you should drill down on the Northwind node, then the Stored Procedures node, and select the spCustomers;1 node. From the Current Data Source combobox, select Customers and then click the Replace button, and, finally, the Close button.
      You have now retrofitted this report—originally designed against the Customers table in the Northwind Database, then converted to use a strongly typed dataset as its data source—to use the spCustomers stored procedure to get its data. Because this is a parameterized stored procedure, Crystal Reports for Visual Studio .NET automatically adds a Crystal parameter field with the same name as the stored procedure parameter to your report. To see this, look in the Field Explorer window, and drill down through the Parameter Fields node to reveal the @CustPattern parameter field right below it (Figure 8 shows its location).

Figure 8 Parameters
Figure 8 Parameters

      Now you can once again set cvwMain.ReportSource, either at design time or in code, to point to the rpt file, or to cbsMain, the ReportDocument component designer instance. If you run the application, you will see that Crystal Reports for Visual Studio .NET automatically invokes a dialog box that requests a value for the @CustPattern parameter. Enter any string pattern you'd like, and and it will return only the customers whose CompanyName field value starts with that string.
      While it's nice that Crystal Reports for Visual Studio .NET can solicit parameter values and run the report without writing code, in most cases, it's better for applications to solicit those values and pass them to the report programmatically. Figure 9 shows the code necessary for programmatic assignment of parameter values through the Crystal Reports object model, in this case once again assigning the arbitrary value of "A" as the parameter of pdvCustPattern.
      Some explanation of this code is necessary. The confusing part is that even though T-SQL parameters can be assigned only a single value, Crystal Reports for Visual Studio .NET parameter fields can be assigned a collection of values. So I have to go along with this and construct a collection that has only one member, assigning "A" as its value. Accordingly, the code creates pvCustPattern, an instance of the CrystalDecisions.Shared.ParameterValues object that accommodates multiple values, and pdvCustPattern, an instance of the CrystalDecisions.Shared.ParameterDiscreteValue class that accommodates a single value. The Value property for pdvCustPattern is assigned the string "A" and the object is loaded into pvCustPattern through the Add method. Finally, pvCustPattern is assigned to the parameter field via its ApplyCurrentValues method, and the instance of CustomersBasic is assigned to the report viewer's ReportSource property. While the code may seem a little convoluted, the functionality it delivers makes it worthwhile.

XML Report Web Services

      Let's now investigate the support for XML Web Services in Crystal Reports for Visual Studio .NET. The great news here is that although the features are sophisticated, taking advantage of them is easy; reports can be published as Web Services and then used as the ReportSource for a CrystalReportViewer control.
      To make an XML Report Web Service from CustomersBasic.rpt, open Visual Studio .NET and create a new ASP.NET Web Service project named MSDNCrystalWebService. Delete the Service1.asmx file that is added to the project by default, and add CustomersBasic.rpt to the project. Next, right-click CustomersBasic.rpt in the Solution Explorer, and choose Publish as Web Service from the context menu (see Figure 10). Doing this adds a Web Service file, CustomersBasicService.asmx, to the project. Now build the project. Then, back in the Windows Forms application, set cvwMain.ReportSource to the URL of the Report Web Service, as in the following line of code:

  cvwMain.ReportSource = "https://localhost/MSDNCrystalWebService/" _
& "CustomersBasicService.asmx"


      Alternately, you can add a Web reference for this asmx file to the project and then point cvwMain.ReportSource to an instance of the Web Service class.

  cvwMain.ReportSource = New localhost.CustomersBasicService()


      How can it be so easy to create an XML Web Services interface to Crystal Reports? Look closely at the code in the class file indented underneath the CustomersBasic.rpt file (click the Show All Files button in the Solution Explorer, if necessary, to reveal the file). There are two classes in the code in this file: CustomersBasic and CachedCustomersBasic.
      CustomersBasic is the primary class needed to implement the strongly typed report; if you look at its property procedures, you will see that it primarily exposes the report and its various sections, by inheriting from ReportClass and wrapping various members of the ReportDefinition.Sections collection. CachedCustomersBasic, in turn, implements the ICachedReports interface to create a cached report, and in its CreateReport method, it creates an instance of CustomersBasic (the first class). Using an interface similar to this one that uses WebMethods, it's possible to expose the report as an XML Web Service.
      Now look at the code in the class file underneath CustomersBasicService.asmx. This one also contains two classes, though this time one is nested within the other. CustomersBasicService inherits from ReportServiceBase, which in turn exposes the Web methods necessary to publish the report as an XML Web Service. CustomersBasicService contains class CachedWebCustomersBasic, which has essentially the same implementation as the CachedCustomersBasic class that was just discussed. One difference can be found in its constructor:

  Public Sub New(ByVal webServiceParam As CustomersBasicService)
Me.webService = webServiceParam
End Sub


The constructor is called from CustomersBasicService's own constructor to point the cached report's webService property to it:

  Public Sub New()
Me.ReportSource = New CachedWebCustomersBasic(Me)
End Sub


      To sum up: Crystal Report's ReportClass makes strongly typed reports possible; the ICachedReports interface makes it possible to create cached instances of ReportClass; and ReportServiceBase effectively creates a Web Services interface for strongly typed reports, which is compatible with the ICachedReports interface through the latter's webService property.
      One more thing. Remember the ServerFileReport component that I created by dragging and dropping from the Server Explorer? It uses Web Services technology as well. If you return to the Windows Form project, you'll notice that sfrCustomersBasic has a WebServiceURL property, which points to a generic .asmx file. The URL looks something like localhost/crystalreportwebformviewer/ ServerFileReportService.asmx. Although this URL is generic, sfrCustomersBasic's ReportPathProperty, pointing to \MSDN\ CustomersBasic.rpt, specifies the relative location and name of the report I actually want to run.
      Now it may seem odd that the ServerFileReport component requires you to run the report via an HTTP/SOAP connection to your local machine, but that is in fact what it's doing. However, this means you could easily right-click on the Servers node in the Server Explorer, choose Add Server, connect to any other server on your LAN/WAN, and run Crystal Reports for Visual Studio .NET reports that exist on that remote machine. This makes it very easy to deploy reports throughout the enterprise: simply install Visual Studio .NET on a server, copy rpt files to the appropriate folder, and they become immediately available throughout the organization (via SOAP) without requiring any special coding. Even the relatively simple process of right-clicking a report in an ASP.NET project and selecting "Publish as Web Service" is not necessary; all you have to do is copy the file to the correct folder.
      The disadvantage of using drag and drop in Server Explorer is that only objects on servers that reside in the developer's own LAN/WAN, not those behind a firewall, can be browsed. But there's a workaround: as long as the URL and ReportPath of a given report are known, and the server (firewalled or not) is connected to the Internet, you can create a new instance of CrystalDecisions.ReportSource.ServerFileReport and set these properties on it in code.
      This easy-to-use SOAP interface is so convenient and straightforward that Crystal Reports for Visual Studio .NET uses it as a wrapper of sorts for its earlier Crystal Enterprise product. The Crystal Enterprise node under Crystal Services in the Server Explorer can be used to browse reports residing on any Crystal Enterprise server on the LAN/WAN and use them as valid ReportSources for a CrystalReportViewer control, again using SOAP. Now the Crystal Enterprise reports are available to any Internet-connected Crystal Reports for Visual Studio .NET developers armed with the correct URL and object ID.

Building ASP.NET Reporting Clients

      So far, everything I have done on the client has been in a Windows Forms application, but you may want to present reports in ASP.NET Web applications. Fortunately, Crystal Reports for Visual Studio .NET provides support for ASP.NET. I'll now show you how to present reports using the ASP.NET Web Forms CrystalReportViewer control as HTML and as Acrobat (PDF) documents. I'll also take a look at caching and using cached data.
      I can cover the basics fairly quickly because the Web Forms CrystalReportViewer works quite similarly to its Windows Forms counterpart. To get started, create a new ASP.NET Web Application in Visual Studio .NET, and use the Add Existing Item feature to add CustomersBasic.rpt to the project. Set the report's data location back to the Customers table, and for testing purposes use a login with a blank password. Next, rename the default WebForm to ViewReport.aspx, and add a Web Forms CrystalReportViewer control to it, setting its ID property to cvwMain. Since the Web Forms CrystalReportViewer control does not have a ReportSource property at design time, you'll have to set its ReportSource property in the Page_Load event of ViewReport.aspx as follows:

  cvwMain.ReportSource = Server.MapPath("CustomersBasic.rpt")


Run the application and you'll see that this works well. The report is served as pure HTML, but the search, navigation, zoom, and other viewer control features are supported (except printing—more on that later), albeit through the use of postbacks. You can try all the other modes of loading a report that I have discussed: reporting from a strongly typed or untyped DataSet, from a stored procedure, using a ReportDocument or a ServerFileReport component designer, and so forth. The code I used previously for configuring the report's security information and parameter values, and for manipulating ADO.NET objects, can be copied into the Page_Load event of ViewReport.aspx. This is similar enough to what I have already done that I won't go though each scenario again, but the ReportDocument option bears some explanation.

Cached ReportDocuments

      Let's explore the cached reports features of Crystal Reports for Visual Studio .NET by trying it out. Drag a ReportDocument component designer onto ViewReport.aspx's designer surface, and select ProjectName.CustomersBasic from the Name: combobox in the Choose a ReportDocument dialog, but don't click the OK button yet. Notice the "Generate cached strongly typed report" checkbox (see Figure 11).

Figure 11 Cached Reports Feature
Figure 11 Cached Reports Feature

Check this checkbox. Click OK and rename the generated component to ccbMain. Next, bind the viewer control to this object in the Page_Load event with this line of code:

  cvwMain.ReportSource = ccbMain


      Next, drag Data Time and Print Time controls to the page header section of the report. These controls can be found in the Field Explorer window under the Special Fields node. Now run the application and view the report. The first time you run it, the Data Time and Print Time should be the same (or very close). But if you refresh the page, you'll see that Print Time continues to be updated while Data Time stays constant. Now copy the browser's current URL to the clipboard, and close the browser. Open a new browser window and paste in the URL to view the report in a new session (without rebuilding the assembly from Visual Studio .NET). You'll see that the original time stamp continues to be displayed for Data Time.
      Now, close the browser and go back to Visual Studio .NET. Once again, drag a ReportDocument component designer onto ViewReport.aspx's designer surface, and select ProjectName.CustomersBasic making sure the "Generate cached strongly typed report" checkbox is unchecked. Click OK, and rename the generated component "cbsMain." After completing that task, bind the viewer control to this object in the Page_Load event making a slight change to the code to reflect the name of the new component, like so:

  cvwMain.ReportSource = cbsMain


      Run the application again to view the report and refresh the page a few times. You'll see that Data Time changes each refresh. This demonstrates the basic difference between a cached and a noncached report: caching prevents needless round-trips to the database when you already have the data you need.
      While you can use the DataDefinition or Database properties of cbsMain (an instance of my noncached ReportDocument) to set parameter values or table logon properties, you'll notice that ccbMain (an instance of the cached ReportDocument) has no such properties. How then do you use cached reports against secured tables and parameterized stored procedures? The key lies in the code behind the strongly typed report. Once again, click the Show All Files button in the Solution Explorer and drill down on CustomersBasic.rpt to reveal the code file behind it. You have been here before, but this time look at the code for the CachedCustomersBasic class in this file.
      Notice that the code for its CreateReport method actually creates a new instance of the noncached class, CustomersBasic; that's because CachedCustomersBasic actually wraps CustomersBasic. Thus, I can write customized code in this public method to access the Database and DataDefinition properties of the created instance of CustomersBasic and interface with secured database objects and parameterized stored procedures. If I take this approach, then when the new instance of the cached report is created, it will, in turn, create an appropriately configured instance of the noncached strongly typed report.
      Now, scroll down a bit further and you'll see code, most of it commented out, for another important method, GetCustomizedCacheKey. This method generates a unique key that determines whether or not a copy of a cached report can be used to fulfill a request. Basically, if the key matches that of a cached report, then the cached copy is used. If the key does not match, then data is fetched anew.
      If you uncomment the code, a special function called BuildCompleteCacheKey generates a key that will be unique for a specific report. Uncommenting the code, but leaving it otherwise unmodified, is functionally equivalent to leaving the code commented. If the key is unmodified, Crystal Reports implicitly uses BuildCompleteCacheKey's algorithm to generate a key for you. For many cases, this is fine. But what if you wanted to modify the key slightly for your own purposes? Suppose you knew, for example, that the data changed significantly at a certain time. You could append your own string to the end of the cache key to make sure it changed at, or after, that time. Here's a trivial example: uncomment the code in GetCustomizedCacheKey and insert the following line before the Return statement:

  key &= DatePart(DateInterval.Minute, Now())


This ensures that whenever the current time changes to a new minute, a new cache key is generated. In ViewReport.aspx's Page_Load event, change cvwMain.ReportSource back to point to ccbMain, run the application, and refresh the page several times. You should see the Data Time change only when the actual time changes; that is, at the exact point in time when the minutes place is incremented. A similar technique can be used for appending parameter values and other data-related specifics to the cache key to prevent distinct runs of the same report from being handled by the same cached data. Rather than merely appending something to the default cache key, you could create your own key, entirely from scratch; I wouldn't recommend this approach though, as it's reinventing the wheel.
      While these changes to the strongly typed report class are nice, the fact remains that you're in some danger here. If you update the report in any way, Visual Studio .NET tries to overwrite the class file for the report; if you're not careful, your changes could be lost. One way to avoid this is to create your own class that implements ICachedReport, which basically contains the code from the CachedCustomersBasic class. Then, in ViewReport.aspx's Page_Load event, you can create an instance of this class and set cvwMain.ReportSource to point to it. Figure 12 has the code for the class (including code that uses the ASP.NET Request object to set the report's parameter field value and to append that value to the cache key), and the following code shows the Page_Load event code required to use it.

  Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load

Dim mcrMain As New cMSDNCachedReport()
mcrMain.ASPRequest = Request
cvwMain.ReportSource = mcrMain
End Sub


Exporting to PDF

      The strength of the Web Forms CrystalReportViewer control—it renders reports as pure HTML—is also its weakness. While this makes for maximum compatibility between Crystal Reports for Visual Studio .NET and client browsers, HTML doesn't allow for precise pagination in printed output.
      One way to work around client-side printing issues is to export your report to Adobe Acrobat (PDF) format. It's possible, and fairly simple, to write ASP.NET code that programmatically exports a report to PDF and then redirects the browser to that same file. In this way, the user can request a report through a postback event and be immediately routed to a PDF version of the report, which can be printed. Figure 13 shows the code necessary to do this. For simplictly, I use the noncached ReportDocument object cbsMain to generate the report.
      If you decide to use PDFs for report presentation, you must be aware of the shortcomings of this approach. First, to accommodate multiple users, the PDF files you generate must be given unique names; Figure 13 uses a literal, constant string, it serves only to demonstrate the technique in isolation. Also, the PDF files must be cleaned up at some point, and, depending on the amount of reporting done in your application, all the file I/O related to PDF generation and cleanup could severely affect scalability if not properly managed and configured. If you're building ASP.NET applications using Crystal Reports for Visual Studio .NET, take some time to map out your client-side viewer and printing strategy; each option has its own set of significant pros and cons.

The Finish Line

      I've provided a very detailed look at the developer-targeted features of Crystal Reports for Visual Studio .NET, and there's still plenty more to investigate. If nothing else, you should come away from all this material with a sense of the richness of these features and how well they are integrated into the .NET Framework and Visual Studio .NET.

For related articles see:
Crystal Decisions
Visual Studio
For background information see:
.NET Framework SDK
SQL Server information

Andrew J. Brust is President of New York City-based Progressive Systems Consulting Inc., a firm specializing in custom application development with .NET and other Microsoft technologies. In addition, Andrew is MSDN Regional Director for New York City and a Vice Chair of the New York Software Industry Association.

From the May 2002 issue of MSDN Magazine