Practical ASP.NET

Let Users Save From ASP.NET

Export the content of your ASP.NET pages to standard Office document types to avoid having to create separate reports.

Technology Toolbox: C#, ASP.NET

Your users will want to save data from your pages at some point in your Web site's lifecycle. Typical Web pages neither save well nor print well, so whole product lines have evolved to generate reports to get data out of a system.

These products do their jobs well, but they require a completely separate development effort. Fortunately, you can simply export your existing pages rather than create a separate system. Simply plug into the ASP.NET framework to intercept the HTML you generate, then reuse the work others have done already to convert HTML to the document formats your users are accustomed to using, such as Microsoft Word, Microsoft Excel, or even Adobe's PDF format. Implementing your site in this manner can help you eliminate the need for a separate reporting system within your site.

You need to put only a few elements in place to start exporting your existing ASP.NET code. First, you must give the user the option to export the desired page to the approved document formats. Second, you must intercept the HTML your page generates and convert it to the appropriate document format. Finally, you need to send the converted document rather than the original HTML to the user.

You spread the code to do this across four elements: the Web page, the export provider manager, the export provider, and the list of exports defined for the page. The Web page registers the list of exports with the manager. The manager generates the options and intercepts the HTML, but the Web page must pass control to the manager for this to occur. The manager also instantiates the appropriate export provider, and that provider streams the converted document to the client (see Figure 1).

Note that you can plug the export functionality into the architecture at any point where you can intercept the HTML and alter the stream back to the client. I chose the templated Web page approach to keep interaction with the page as simple as possible.

Your Web site should utilize a common base class. In addition to providing the coordination with the export provider manager, this base page can provide a consistent look and feel through any number of template implementations. The approach described here uses a derivative of the WebForm Template pattern documented in Christian Thilmany's .NET Patterns: Architecture, Design, and Process to implement a common base page for the site.

Override the Render Method
The general idea of the WebForm Template pattern is to use the base class to derive from the standard System.Web.UI.Page and then override the Render method. The override provides a standard header and footer for each page, including logo, menus, or copyrights. TemplateTop and TemplateBottom methods exist in this base class to generate and return HTML strings of the opening and closing of the HTML document, respectively. The Render method's override requires only a couple lines of code:

protected override void 
   Render(HtmlTextWriter writer)
{
   writer.Write(TemplateTop());
   
   base.Render(writer);

   writer.Write(TemplateBottom());
}

When developing new pages, you remove the standard opening and closing HTML tags generated when you add a new Web form to your project. Also, you change the base class of the page to the templated base page and need to worry only about the content you want displayed on the page.

This article extends this implementation by providing virtual methods for generating common areas of the page. For example, you can generate the HTML for the menus in the virtual method, GetTagMenus. A page that wants to alter the display of the menu needs only to override that method and return the specialized HTML.

The export providers handle the task of getting the HTML from your page into the appropriate format and appropriate application on the user's machine. Each provider implements a standard interface to do this:

interface IExportProvider
{void StreamOutput(
   string HTML, 
   HttpResponse OutputStream);
   Uri BaseLocation { get; set; }
string DownloadAsFileName {get; set;}
string ContentType { get; }        
object UnderlyingDoc { get; }
PreStreamDelegate PreStream { set; }
}

The DownloadAsFileName and ContentType properties on the interface provide the export provider manager information for initializing the response stream to the browser. The StreamOutput method performs the HTML conversion. Two additional properties, UnderlyingDoc and PreStream, provide extensibility points for greater control over the document that you send to the client.

The export provider definition is a simple class intended to hold all the properties that define a specific export provider. In this example, this includes display information for the export link and the actual System.Type of the class that will perform the export. A unique ID is assigned to each export to tie the links on the page to the specific exports to be performed.

The export provider manager is the traffic cop of the export process. Pages derived from the templated base class register the export provider definitions with the manager and allow the manager to take care of determining whether an export is being performed. If an export is being performed, the manager initializes the response stream for the client, and coordinates with the specific export providers to perform the export.

I also included code to generate the HTML for the export links to provide as much of a "drop-in" implementation as possible (download the code here). Even if you choose to change my simple implementation, you can adapt this class easily to any existing Web site once you're happy with how the export links look.

Create a Simple Sample
The sample site consists of a simple page that provides a listing of customers (borrowed from the Northwind database) for the fictional Widgets Exports Incorporated. The page includes a logo, a simple banner area at the top, and a menu listing down the left column. You'll need to expand this page to provide the various exports. Each of the four pages in the site corresponds with a step in the evolution of the exports.

The "database" for the site is nothing more than an XML file stored in the site's directory. The base page includes functions for loading this into a dataset for use by the page. The project implements all base classes and export functionality in the Web project to simplify sharing the project.

Begin the process of adding exports by creating an export to Microsoft Word. Recent versions of Word can read HTML already, so all you need to do is tell the browser to treat the HTML you're sending as a Word document rather than rendering it in the browser window (see Listing 1). Set the DownloadAsFileName in the constructor to a filename with a DOC extension. Combined with the ContentType of "application/msword," this instructs the browser to hand the HTML off to the registered application (Microsoft Word, typically).

The WordHTML class calls a helper method to "rebase" any relative paths on image tags to absolute paths before sending the HTML to the response stream. For example, it converts an image reference of "<img src='images/logo.gif'>" to "<img src='http://local/AppDir/images.logo.gif'>." This means you don't have to change all your image references.

Exporting files in Excel format varies only marginally from the implementation used to export files in Word format. The only differences are that you specify "export.xls" as the DownloadAsFileName and that you return the ContentType as "application/vnd.ms-excel."

A number of third-party components exist to convert HTML to a variety of file types, including Adobe's PDF format. If you're using one of these components, your UnderlyingDoc property returns a reference to the object that will be used to convert the HTML, and the StreamOutput method calls the components method to do the conversion. Although you can read and stream a file to the client manually, ideally you should search for one that streams the document to an output stream directly.

Implement Base Page Changes
So far you've defined a couple export providers. Next, you need to modify the templated base page to use them. The export provider manager provides the bulk of the implementation, so the changes to the base page are minimal (see Listing 2). You need to add members for the export provider manager and the current export you want to perform. Add an "empty" virtual method called RegisterExportProviders that derived pages can override to indicate which exports should be available, and make a call to RegisterExportProviders in the OnInit override. You also need to call the export provider manager in OnInit to determine whether an export has been requested and modify the Render override.

In your Render override, call the manager's InitializeResponse method to initialize the export process and render your page to the HtmlTextWriter returned by the manager instead of the original writer (see Listing 3). If no export is being performed, the InitializeResponse method returns the original writer, and your page will render as normal. If an export is being performed, the export provider manager returns a new HtmlTextWriter wrapped around a string builder. This lets you store the HTML your page renders, but not send it to the client—yet.

At the end of the Render method, you call export provider manager's CompleteResponse to finalize the conversion. If no export is being performed, nothing happens. If one is being performed, the manager hands the HTML generated off to the selected export provider to convert and stream to the client.

Two additional virtual methods, GetTagExports and GetTagExportScripts, handle including the export UI elements and the JavaScript to show them, respectively. The TemplateTop and TemplateBottom methods call these virtual methods in the same manner as GetTagMenus.

You now have a framework in place to register export providers for a Web page with a single line of code. These lines from the Simple Exports page show how you can register Word and Excel in a page derived from the base page:

public override void 
   RegisterExportProviders()
{
   mgrExports.RegisterExportProvider(
      new ExportProviderDefinition(1, 
      "Export to Excel", 
      typeof(ExcelHTML)));
   mgrExports.RegisterExportProvider(
      new ExportProviderDefinition(2, 
      "Export to Word", 
      typeof(WordHTML)));
}

The ExportProviderDefinition class comes with a unique ID, the text that should be shown to the user, and the export provider type the manager uses to create an instance of the object.

Strip Out Unneeded Elements
At this point, you have an exact replica of the page in Word and Excel formats, but that's far from ideal. For example, you should eliminate the menus, the search form, and the export link functionality when you do an export, so they don't show in the converted document. Also, the filter doesn't get passed to the export process correctly.

Add these lines to the Page_Load method to turn off the search form and register the current filter settings with the export provider manager:

if (mCurrentExport != null)
{
   tblSearchForm.Visible = false;
}

string strFilter = Request["txtFilter"];
if (strFilter == null) 
   strFilter = "";
else
   mgrExports.RegisterExportValue(
      tFilter", 
      strFilter);

The first line checks to see whether an export is in process. If so, it turns off the search form. The second part illustrates how to register the filter criteria with the manager.

The Controlling Exports page overrides the GetTagMenus, GetTagExports, and GetTagExportScripts methods to control the menus and export processes. Note that you would typically perform this action directly in the base page, but I left it in as an override here to show the progression. Each override checks to see whether an export is in progress (see the Page_Load event) and, if so, returns an empty string. If not, the override calls the base implementation, leaving the menu and export functionality in place.

If you want to eliminate the need for reports, you need to customize the data for the export type you target. For example, if you send data to Excel, the users want only the data. You also want the ability to eliminate certain navigational features in the site, such as paging through larger data sets when you do the export. The final page in the sample site shows how you can modify the content for the export process and export type.

You can use the mCurrentExport member's ID property to know what export is being performed. You perform this check in an override of the TemplateTop and TemplateBottom methods. If the ID matches the Excel export's ID, you can return the absolute minimum number of tags necessary to create a valid HTML document, effectively eliminating the templates.

You can do the same if you're using a third-party component to perform the conversion for the export. Do this by getting a handle on the UnderlyingDoc and, if it is the third-party component, set the appropriate properties on the component returned. Many components will expose properties to set the type of headers, footers, and page margins.

You can also set the PreStream delegate of the export provider to notify you just prior to streaming the document to the client. This gives you a chance to make last-minute modifications to the document. If you have special conversions you'd like to make to the HTML before it is sent or specific properties you'd like to set on a third-party component, do it in your delegate.

You can use this simple framework to provide your users the functionality they need with only a minimum of additional effort. The sample provides some simple exports, but you can export your page to any document as long as you can get there from HTML. This approach saves you from building a separate subsystem to generate reports when what your users need is only a way to save the data on their screen. It can also save you some effort by letting you reuse the pages you've created already to export to the document types your users know.

comments powered by Disqus

Featured

Subscribe on YouTube