ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


O'Reilly Book Excerpts: ASP.Net Cookbook

Cooking with ASP.NET, Part 2

by Mikkel Aaland, Geoffrey T. LeBlond

Editor's note: Last week, in part one of this two-part excerpt from the ASP.NET Cookbook , authors Michael Kittel and Geoffrey LeBlond cooked up three recipes to make ASP.NET work for you. This week, they're back in the kitchen with two more recipes: one to create a reusable handler that reads image data from the database and sends it to the browser, and another to improve the performance of pages that rarely change by saving and reusing HTML output.

Recipe 17.1: Creating a Reusable Image Handler

Problem

You want to create a reusable assembly that retrieves image data from a database and processes it before sending it to a browser.

Related Reading

ASP.NET Cookbook
The Ultimate ASP.NET Code Sourcebook
By Michael A. Kittel, Geoffrey T. LeBlond

Solution

Create an HTTP handler to read the image data from the database and send it to the browser.

To implement a custom, reusable HTTP handler:

  1. Create a separate Class Library project in Visual Studio.

  2. Create a class in the project that implements the IHttpHandler interface and place code to handle the request in the ProcessRequest method.

  3. Compile the project as an assembly and place the assembly in the bin directory of your web project.

  4. Add an <httpHandlers> element to the web.config file in your web project referencing your custom HTTP handler.

  5. Reference the URL of the HTTP handler in your application.

Example 17-3 and Example 17-4 show the VB and C# class files we've written to implement an image handler as an HTTP handler. Example 17-5 through Example 17-7 show the .aspx file and VB and C# code-behind files for our application that demonstrates the use of the HTTP handler.

Discussion

HTTP handlers are simply classes that implement the IHttpHandler interface. Implementing the IHttpHandler interface requires the implementation of two methods: IsReusable and ProcessRequest. IsReusable is a property that explicitly returns a Boolean value that indicates whether the HTTP handler can be reused by other HTTP requests. For synchronous handlers, like our example, the property should always return false so the handler is not pooled (kept in memory). The ProcessRequest method is where the actual work is performed and you should place code that processes the requests here.

To create a reusable HTTP handler, you need to eliminate all application-specific code from the class. You must also compile the class as a separate .NET assembly and place the assembly in the bin directory of each application that uses it.

To create an assembly that contains only the handler code, you need to create a separate Class Library project in Visual Studio. In our example, we have named the project VBImageHandler (or CSImageHandler for C#) resulting in an assembly that has the same name as the project. We then compile the assembly, place it in the bin directory of the web project, and add a reference to the assembly.

TIP: When you create a new "Class Library" project for your HTTP handler, you will need to add a reference to the System.Web assembly. This is required because the IHttpHandler interface and HttpContext class used by the HTTP handler are defined in the System.Web assembly.

In our example that demonstrates this solution, we have stored GIF images in a database that can be retrieved and displayed in a browser using our HTTP handler just as if they were standard image files. To demonstrate the HTTP handler, we created an ASP.NET page that contains a DropDownList, a View button, and an HTML img tag. The DropDownList displays the descriptions of the images stored in the database. When you make a selection from the list and click the View button, the src attribute for the img tag is set to the URL of our HTTP handler with the ID of the image in the URL. When the page is then displayed, the browser requests the image from our HTTP handler, which retrieves the ID of the requested image from the URL, reads the data from the database for the image, and then streams the image data to the browser. Figure 17-1 shows the output of the page used to test our HTTP handler.


Figure 17-1. Output from HTTPHandler test page

The image handler implemented in our example needs several pieces of data to retrieve an image from the database. These include the following:

To be reusable, none of this data can be coded directly into the handler. To get around this problem in our example, we declare four public constants in the image handler class that we can use to specify the names of the variables in Application scope that contain the database information. In addition, the ID of the image that is to be downloaded will be passed in the URL used to access the handler (described later).

The application variables defined by the constants are initialized in the Application_Start method of the global.asax.vb (VB) or global.asax.cs (C#) class, which is executed when an application is first started. If you initialize your application variables in the Application_Start method, they will always be available when HTTP requests are processed. The code to implement this approach is in Example 17-1 and Example 17-2.

Example 17-1. Application variable initialization for image handler (.vb)

Imports ASPNetCookbook.VBExamples.HttpHandlers

Namespace ASPNetCookbook.VBExamples
 Public Class Global
 Inherits System.Web.HttpApplication

 Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
 Dim strConnection As String

      'get the connection string from web.config
      strConnection = _
          ConfigurationSettings.AppSettings("dbConnectionString")
      'Set application variables used in image HTTP handler example
      Application.Add(ImageHandlerVB.APP_CONNECTION_STR, strConnection)
      Application.Add(ImageHandlerVB.APP_IMAGE_TABLE, "BookImage")
      Application.Add(ImageHandlerVB.APP_IMAGE_ID_COLUMN, "BookImageID")
      Application.Add(ImageHandlerVB.APP_IMAGE_DATA_COLUMN, "ImageData")
 End Sub 'Application_Start
 End Class 'Global
End Namespace

Example 17-2. Application variable initialization for image handler (.cs)

using ASPNetCookbook.CSExamples.HttpHandlers;

namespace ASPNetCookbook.CSExamples 
{
  public class Global : System.Web.HttpApplication
  {
    protected void Application_Start(Object sender, EventArgs e)
    {
      String strConnection = null;

      // get the connection string from web.config
      strConnection = 
        ConfigurationSettings.AppSettings["dbConnectionString"];
      // Set application variables used in image HTTP handler example
      Application.Add(ImageHandlerCS.APP_CONNECTION_STR, strConnection);
      Application.Add(ImageHandlerCS.APP_IMAGE_TABLE, "BookImage");
      Application.Add(ImageHandlerCS.APP_IMAGE_ID_COLUMN, "BookImageID");
      Application.Add(ImageHandlerCS.APP_IMAGE_DATA_COLUMN, "ImageData");
    }  // Application_Start
  }  // Global
}

To create the image handler, you next need to create a class that implements IHttpHandler and its two methods: IsReusable and ProcessRequest. Add the code to process requests made to the handler to the ProcessRequest method. As mentioned, IsReusable is a property that returns a Boolean value indicating whether the HTTP handler can be reused by other HTTP requests. Because our example is a synchronous handler, the property returns false so the handler is not pooled (kept in memory).

In our example, the first step in processing a request for an image is to get the ID of the requested image from the URL that is being processed by the handler.

Next, a connection to the database needs to be opened. The connection string is obtained from an Application scope variable defined by the APP_CONNECTION_STR constant shown in Example 17-1(VB) and Example 17-2 (C#). The name of the database table along with the columns containing the unique identifier and the image data are also obtained from the Application scope variables described earlier. These are then used to create the SQL statement required to read the image data from the database.

The next step in our example is to read the image data from the database using the ExecuteScalar method of the command object. The ExecuteScalar method returns a generic Object, so the return value must be cast to the type of data stored in the database. In this case it must be cast to a byte array.

The image data stored in the database for our example is in the GIF format, so the content type is set to "image/GIF" to inform the browser of the type of data being sent. After setting the content type, the image data is written to the Response object using the BinaryWrite method.

TIP: If your image is of another type, you will need to set the ContentType accordingly. Other choices for images include "image/jpeg", "images/tiff", and "images/png".

In order to use the handler, we next have to add information to the <httpHandlers> element of the web.config file of the application to tell ASP.NET which URL requests it should route to our custom image handler. You insert this information using an add element and its attributes. The verb attribute defines the types of requests that are routed to the HTTP handler. The allowable values are *, GET, HEAD, and POST. The value * is a wildcard that specifies that all request types are to be routed to the handler.

The path attribute defines the URL(s) that are to be processed by the HTTP handler. The path can be set to a single URL, or to a less specific value such as "*.images" to have the HTTP handler process all requests for URLs with an images extension. In our example, we are setting the path to a specific URL (ImageHandlerVB.aspx).

TIP: IIS routes requests with the extensions .asax, .ascx, .ashx, .asmx, .aspx, .axd, .config, .cs, .csproj, .lic, .rem, .resources, .resx, .soap, .vb, .vbproj, .vsdisco, and .webinfo to ASP.NET for processing.

To use an HTTP handler for requests with other extensions, IIS must be configured to send the requests with the desired extensions to the aspnet_isapi.dll.

The type attribute defines the name of the assembly and class within the assembly that will process the request in the format type="class name, assembly". The class name must be identified by its full namespace. Here is the code necessary to add a reference to the image handler to an application web.config file:


VB
<configuration>
  <system.web>
    <httpHandlers>
      <add verb="*" path="ImageHandlerVB.aspx" 
           type="ASPNetCookbook.VBExamples.HttpHandlers.ImageHandlerVB, 
                VBImageHandler" />
    </httpHandlers>
  </system.web>
</configuration>

C#
<configuration>
  <system.web>
    <httpHandlers>
      <add verb="*" path="ImageHandlerCS.aspx" 
           type="ASPNetCookbook.CSExamples.HttpHandlers.ImageHandlerCS, 
                CSImageHandler" />
    </httpHandlers>
  </system.web>
</configuration>

To use the HTTP handler to retrieve images from the database, we need to set the src attribute of image tags that will use the HTTP handler to the name of the HTTP handler defined in the path attribute of the entry added to web.config, passing the ID of the desired image in the URL. In our example, the src attribute of an img tag is set in the view image button click event of the test page code-behind. A sample URL is shown here:

src="ImageHandlerVB.aspx?ImageID=5"

TIP: The HTTP handler does not have to be implemented in the same language as the application. The C# image handler can be used in VB projects or vice versa.

Example 17-3. Image HTTP handler (.vb)

Option Explicit On 
Option Strict On
'-----------------------------------------------------------------------------
'
'   Module Name: ImageHandlerVB.vb
'
'   Description: This class provides an image handler as an HTTP handler.
'
'*****************************************************************************
Imports Microsoft.VisualBasic
Imports System.Configuration
Imports System.Data
Imports System.Data.OleDb
Imports System.Web

Namespace ASPNetCookbook.VBExamples.HttpHandlers
  Public Class ImageHandlerVB
    Implements IHttpHandler

    'The following constant is used in the URL used to access this handler to
    'define the image required
    Public Const QS_IMAGE_ID As String = "ImageID"
    'The following constants define the name of the application variables 
    'used to define the database connection string and database table
    'information required to retrieve the required image
    Public Const APP_CONNECTION_STR As String = "DBConnectionStr"
    Public Const APP_IMAGE_TABLE As String = "DBImageTable"
    Public Const APP_IMAGE_ID_COLUMN As String = "DBImageIDColumn"
    Public Const APP_IMAGE_DATA_COLUMN As String = "DBImageDataColumn"

    '***************************************************************************
    '
    '   ROUTINE: IsReusable
    '
    '   DESCRIPTION: This property defines whether another HTTP handler can 
    '                reuse this instance of the handler.
    '
    '       NOTE: False is always returned since this handler is synchronous
    '             and is not pooled.
    '---------------------------------------------------------------------------
    Public ReadOnly Property IsReusable( ) As Boolean _
      Implements IHttpHandler.IsReusable
      Get
        Return (False)
      End Get
    End Property  'IsReusable

    '***************************************************************************
    '
    '   ROUTINE: ProcessRequest
    '
    '   DESCRIPTION: This routine provides the processing for the http request.
    '                It is responsible for reading image data from the 
    '                database and writing it to the response object.
    '---------------------------------------------------------------------------
    Public Sub ProcessRequest(ByVal context As HttpContext) _
      Implements IHttpHandler.ProcessRequest
      Dim dbConn As OleDbConnection
      Dim dCmd As OleDbCommand
      Dim strConnection As String
      Dim imageTable As String
      Dim imageIDColumn As String
      Dim imageDataColumn As String
      Dim strSQL As String
      Dim imageID As String
      Dim imageData( ) As Byte
      Try
        'get the ID of the required image from the querystring
        imageID = context.Request.QueryString(QS_IMAGE_ID)
        'get the connection string and open a connection to the database
        strConnection = CStr(context.Application(APP_CONNECTION_STR))
        dbConn = New OleDbConnection(strConnection)
        dbConn.Open( )
        'get the name of the database table and columns where the image
        'data is stored then create the SQL to read the data from
        'the database
        imageTable = CStr(context.Application(APP_IMAGE_TABLE))
        imageIDColumn = CStr(context.Application(APP_IMAGE_ID_COLUMN))
        imageDataColumn = CStr(context.Application(APP_IMAGE_DATA_COLUMN))
        strSQL = "SELECT " & imageDataColumn & _
                 " FROM " & imageTable & _
                 " WHERE " & imageIDColumn & "=?"
        dCmd = New OleDbCommand(strSQL, dbConn)
        dCmd.Parameters.Add(New OleDbParameter("ImageID", _
                                               imageID))
        'get the image data
        imageData = CType(dCmd.ExecuteScalar( ), Byte( ))
        'write the image data to the reponse object
        context.Response.ContentType = "image/gif"
        context.Response.BinaryWrite(imageData)
      Finally
        'clean up
        If (Not IsNothing(dbConn)) Then
          dbConn.Close( )
        End If
      End Try
    End Sub  'ProcessRequest
  End Class  'ImageHandlerVB
End Namespace

Example 17-4. Image HTTP handler (.cs)

//----------------------------------------------------------------------------
//
//   Module Name: ImageHandlerCS
//
//   Description: This class provides an image handler as an HTTP handler.
//
//****************************************************************************
using System;
using System.Data;
using System.Data.OleDb;
using System.Web;

namespace ASPNetCookbook.CSExamples.HttpHandlers
{
  public class ImageHandlerCS : IHttpHandler
  {
    // The following constant is used in the URL used to access this handler 
    // to define the image required
    public const string QS_IMAGE_ID = "ImageID";
    // The following constants defines the name of the application variables
    // used to define the database connection string and database table
    // information required to retrieve the required image
    public const string  APP_CONNECTION_STR = "DBConnectionStr";
    public const string  APP_IMAGE_TABLE = "DBImageTable";
    public const string  APP_IMAGE_ID_COLUMN = "DBImageIDColumn";
    public const string  APP_IMAGE_DATA_COLUMN = "DBImageDataColumn";

    //************************************************************************
    //
    //   ROUTINE: IsReusable
    //
    //   DESCRIPTION: This property defines whether another HTTP handler can 
    //                reuse this instance of the handler.
    //
    //       NOTE: false is always returned since this handler is synchronous
    //             and is not pooled.
    //------------------------------------------------------------------------
    public bool IsReusable
    {
      get
      {
        return(false);
      }
    }  // IsReusable

    //************************************************************************
    //
    //   ROUTINE: ProcessRequest
    //
    //   DESCRIPTION: This routine provides the processing for the http 
    //                request. It is responsible for reading image data from 
    //                the database and writing it to the response object.
    //------------------------------------------------------------------------
    public void ProcessRequest(HttpContext context)
    {
      OleDbConnection dbConn = null;
      OleDbCommand dCmd = null;
      String strConnection = null;
      String imageTable = null;
      String imageIDColumn = null;
      String imageDataColumn = null;
      String strSQL = null;
      String imageID = null;
      byte[] imageData = null;
      try
      {
        // get the ID of the required image from the querystring
        imageID = context.Request.QueryString[QS_IMAGE_ID];
        // get connection string from application scope and open connection
        // to the database
        strConnection = (String)(context.Application[APP_CONNECTION_STR]);
        dbConn = new OleDbConnection(strConnection);
        dbConn.Open( );
        // get the name of the database table and columns where the image
        // data is stored then create the SQL to read the data from
        // the database
        imageTable = (String)(context.Application[APP_IMAGE_TABLE]);
        imageIDColumn = (String)(context.Application[APP_IMAGE_ID_COLUMN]);
        imageDataColumn = (String)(context.Application[APP_IMAGE_DATA_COLUMN]);
        strSQL = "SELECT " + imageDataColumn +
                 " FROM " + imageTable +
                 " WHERE " + imageIDColumn + "=?";
        dCmd = new OleDbCommand(strSQL, dbConn);
        dCmd.Parameters.Add(new OleDbParameter("ImageID",
                                               imageID));
        imageData = (byte[])(dCmd.ExecuteScalar( ));
        // write the image data to the reponse object
        context.Response.ContentType = "image/gif";
        context.Response.BinaryWrite(imageData);
      }  // try
      finally
      {
        // clean up
        if (dbConn != null)
        {
          dbConn.Close( );
        }
      }  // finally
    }  // ProcessRequest
  }  // ImageHandlerCS
}

Example 17-5. Using the image HTTP handler (.aspx)

<%@ Page Language="vb" AutoEventWireup="false" 
         Codebehind="CH17TestHTTPImageHandlerVB.aspx.vb" 
         Inherits="ASPNetCookbook.VBExamples.CH17TestHTTPImageHandlerVB" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
  <head>
    <title>Test HTTP Image Handler</title>
    <link rel="stylesheet" href="css/ASPNetCookbook.css">
  </head>
  <body leftmargin="0" marginheight="0" marginwidth="0" topmargin="0">
    <form id="frmTestImageHandler" method="post" runat="server">
      <table width="100%" cellpadding="0" cellspacing="0" border="0">
        <tr>
          <td align="center">
            <img src="images/ASPNETCookbookHeading_blue.gif">
          </td>
        </tr>
        <tr>
          <td class="dividerLine">
            <img src="images/spacer.gif" height="6" border="0"></td>
        </tr>
      </table>
      <table width="90%" align="center" border="0">
        <tr>
          <td align="center">&nbsp;</td>
        </tr>
        <tr>
          <td align="center" class="PageHeading">
            Test HttpHandler For Images (VB)
          </td>
        </tr>
        <tr>
          <td><img src="images/spacer.gif" height="10" border="0"></td>
        </tr>
        <tr>
          <td align="center">
            <table>
              <tr>
                <td>
                  <asp:DropDownList ID="ddImages" Runat="server" />
                </td>
                <td>
                  <input id="btnViewImage" runat="server" 
                         type="button" value="View">
                </td>
              </tr>
              <tr>
                <td id="tdSelectedImage" runat="server" 
                    colspan="2" align="center" class="SubHeading">
                   <br /><br />
                   Selected Image<br /><br />
                   <img id="imgBook" runat="server" border="0">
                </td>
              </tr>
            </table>
          </td>
        </tr>
      </table>
    </form>
  </body>
</html>

Example 17-6. Using the image HTTP handler code-behind (.vb)

Option Explicit On 
Option Strict On
'-----------------------------------------------------------------------------
'
'   Module Name: CH17TestHTTPImageHandlerVB.aspx.vb
'
'   Description: This module provides the code behind for the 
'                CH17TestHTTPImageHandlerVB.aspx page.
'
'*****************************************************************************
Imports ASPNetCookbook.VBExamples.HttpHandlers
Imports Microsoft.VisualBasic
Imports System
Imports System.Configuration
Imports System.Data
Imports System.Data.OleDb

Namespace ASPNetCookbook.VBExamples
  Public Class CH17TestHTTPImageHandlerVB
    Inherits System.Web.UI.Page

    'controls on the form
    Protected ddImages As System.Web.UI.WebControls.DropDownList
    Protected WithEvents btnViewImage As _
      System.Web.UI.HtmlControls.HtmlInputButton
    Protected imgBook As System.Web.UI.HtmlControls.HtmlImage
    Protected tdSelectedImage As System.Web.UI.HtmlControls.HtmlTableCell

    '***************************************************************************
    '
    '   ROUTINE: Page_Load
    '
    '   DESCRIPTION: This routine provides the event handler for the page load
    '                event.  It is responsible for initializing the controls 
    '                on the page.
    '---------------------------------------------------------------------------
    Private Sub Page_Load(ByVal sender As System.Object, _
                          ByVal e As System.EventArgs) Handles MyBase.Load
      Dim dbConn As OleDbConnection
      Dim dCmd As OleDbCommand
      Dim dr As OleDbDataReader
      Dim strConnection As String
      Dim strSQL As String

      If (Not Page.IsPostBack) Then
        Try
          'initially hide the selected image since one is not selected
          tdSelectedImage.Visible = False
          'get the connection string from web.config and open a connection 
          'to the database
          strConnection = _
              ConfigurationSettings.AppSettings("dbConnectionString")
          dbConn = New OleDbConnection(strConnection)
          dbConn.Open( )
          'build the query string used to get the data from the database
          strSQL = "SELECT BookImageID, Title " & _
                   "FROM BookImage"
          dCmd = New OleDbCommand(strSQL, dbConn)
          dr = dCmd.ExecuteReader( )
          'set the source of the data for the repeater control and bind it
          ddImages.DataSource = dr
          ddImages.DataTextField = "Title"
          ddImages.DataValueField = "BookImageID"
          ddImages.DataBind( )

        Finally
          'clean up
          If (Not IsNothing(dbConn)) Then
            dbConn.Close( )
          End If
        End Try
      End If
    End Sub  'Page_Load

    '***************************************************************************
    '
    '   ROUTINE: btnViewImage_ServerClick
    '
    '   DESCRIPTION: This routine provides the event handler for the view
    '                image click event.  It is responsible for setting the
    '                src attibute of the imgBook tag to the URL of the
    '                HTTP handler that will deliver the image content.
    '---------------------------------------------------------------------------
    Private Sub btnViewImage_ServerClick(ByVal sender As Object, _
                ByVal e As System.EventArgs) _
                Handles btnViewImage.ServerClick
      'set the source for the selected image tag
      imgBook.Src = "ImageHandlerVB.aspx?" & _
                    ImageHandlerVB.QS_IMAGE_ID & "=" & _
                    ddImages.SelectedItem.Value.ToString( )
      'make the selected image visible
      tdSelectedImage.Visible = True
    End Sub  'btnViewImage_ServerClick
  End Class  'CH17TestHTTPImageHandlerVB
End Namespace

Example 17-7. Using the image HTTP handler code-behind (.cs)

//----------------------------------------------------------------------------
//
//   Module Name: CH17TestHTTPImageHandlerCS.aspx.cs
//
//   Description: This module provides the code behind for the 
//                CH17TestHTTPImageHandlerCS.aspx page
//
//****************************************************************************
using ASPNetCookbook.CSExamples.HttpHandlers;
using System;
using System.Configuration;
using System.Data;
using System.Data.OleDb;

namespace ASPNetCookbook.CSExamples
{
  public class CH17TestHTTPImageHandlerCS : System.Web.UI.Page
  {
    protected System.Web.UI.WebControls.DropDownList ddImages;
    protected System.Web.UI.HtmlControls.HtmlInputButton btnViewImage;
    protected System.Web.UI.HtmlControls.HtmlImage imgBook;
    protected System.Web.UI.HtmlControls.HtmlTableCell tdSelectedImage;

    //************************************************************************
    //
    //   ROUTINE: Page_Load
    //
    //   DESCRIPTION: This routine provides the event handler for the page 
    //                load event.  It is responsible for initializing the 
    //                controls on the page.
    //------------------------------------------------------------------------
    private void Page_Load(object sender, System.EventArgs e)
    {
      OleDbConnection dbConn = null;
      OleDbCommand dc = null;
      OleDbDataReader dr = null;
      String strConnection = null;
      String strSQL = null;

      // wire the view button click event
      this.btnViewImage.ServerClick += 
        new EventHandler(this.btnViewImage_ServerClick);

      if (!Page.IsPostBack) 
      {
        try
        {
          // initially hide the selected image since one is not selected
          tdSelectedImage.Visible = false;
          // get the connection string from web.config and open a connection 
          // to the database
          strConnection = 
            ConfigurationSettings.AppSettings["dbConnectionString"];
          dbConn = new OleDbConnection(strConnection);
          dbConn.Open( );
          // build the query string and get the data from the database
          strSQL = "SELECT BookImageID, Title " +
                   "FROM BookImage";
          dc = new OleDbCommand(strSQL, dbConn);
          dr = dc.ExecuteReader( );
          // set the source of the data for the repeater control and bind it
          ddImages.DataSource = dr;
          ddImages.DataTextField = "Title";
          ddImages.DataValueField = "BookImageID";
          ddImages.DataBind( );
        }

        finally
        {
          // clean up
          if (dbConn != null)
          {
            dbConn.Close( );
          }
        }
      }
    }  // Page_Load

    //************************************************************************
    //
    //   ROUTINE: btnViewImage_ServerClick
    //
    //   DESCRIPTION: This routine provides the event handler for the view
    //                image click event.  It is responsible for setting the
    //                src attibute of the imgBook tag to the page that will
    //                retrieve the image data from the database and stream 
    //                it to the browser.
    //
    //------------------------------------------------------------------------
    private void btnViewImage_ServerClick(Object sender, 
                                          System.EventArgs e)
    {
      // set the source for the selected image tag
      imgBook.Src = "ImageHandlerCS.aspx?" +
                    ImageHandlerCS.QS_IMAGE_ID + "=" +
                    ddImages.SelectedItem.Value.ToString( );
      // make the selected image visible
      tdSelectedImage.Visible = true;
    }  // btnViewImage_ServerClick
  }  // CH17TestHTTPImageHandlerCS
}

Recipe 18.6: Saving and Reusing HTML Output

Problem

To improve the performance of pages that rarely change, you want to capture the output of those pages and save it for reuse when the page is requested.

Solution

Create the page that contains the desired content just as you would any other page, including the server controls you need. At the end of the Page_Load method, use the RenderControl method of the Page control to generate the HTML and then save the HTML to a file.

In the code-behind class for the page, use the .NET language of your choice to:

  1. Create an HtmlTextWriter to use for rendering the page.

  2. Use the RenderControl method of the Page control to render the output of the page to the HtmlTextWriter.

  3. Save the rendered output to a file and redirect to another page.

Example 18-16 and Example 18-17 show the VB and C# code-behind files for our application that demonstrates this solution.

Discussion

Occasionally, it's beneficial to save the HTML output from a generated page. This is commonly done when using the saved HTML can significantly improve web site performance. If the content of a page is static, for example, there is no point in dynamically generating HTML each time the page is requested. Until the advent of ASP.NET, the only way to save the HTML was to use the "Save as Complete Web Page" feature of Internet Explorer or another browser. Although this method does save the HTML, it also copies all of the page images to the local machine and changes the image references to point to the local copies. If you are trying to improve performance by capturing a static copy of the page to use on your web server, this technique does not work very well.

With ASP.NET you can easily capture the HTML exactly as it would be sent to the browser. For our example that illustrates this solution, we have used the page from Recipe 18.3 and added code to the Page_Load method to save the rendered output.

The RenderControl method of the Page control provides the ability to render the output of the page to the HtmlTextWriter passed to the method. Unfortunately, the HtmlTextWriter does not provide any methods for reading the contents, so a little more work is required to access the rendered HTML.

By first creating a StringBuilder and then using it to create a StringWriter, which is then used to create the required HtmlTextWriter, the contents of the HtmlTextWriter are available by way of the original StringBuilder. This works because the underlying storage mechanism for the StringWriter is a StringBuilder; and because the StringWriter (a stream) is used to create the HtmlTextWriter, the RenderControl method is actually writing the rendered output to the StringBuilder. Our example code to accomplish this is shown here:


VB
  renderedOutput = New StringBuilder( )
  strWriter = New StringWriter(renderedOutput)
  tWriter = New HtmlTextWriter(strWriter)

C#
  renderedOutput = new StringBuilder( );
  strWriter = new StringWriter(renderedOutput);
  tWriter = new HtmlTextWriter(strWriter);

After creating the HtmlTextWriter, the RenderControl method of the Page is called to render the HTML for the page:


VB
  Page.RenderControl(tWriter)

C#
  Page.RenderControl(tWriter);

Now that the rendered HTML is available, it needs to be saved to a file on the server. This can be accomplished by creating the file with a FileStream and using a StreamWriter to write the rendered output in the StringBuilder to the file, as shown here:


VB
  filename = Server.MapPath(".") & "\" & OUTPUT_FILENAME
  outputStream = New FileStream(filename, _
                                FileMode.Create)
  sWriter = New StreamWriter(outputStream)
  sWriter.Write(renderedOutput.ToString( ))
  sWriter.Flush( )

C#
  filename = Server.MapPath(".") + "\\" + OUTPUT_FILENAME;
  outputStream = new FileStream(filename,
                                FileMode.Create);
  sWriter = new StreamWriter(outputStream);
  sWriter.Write(renderedOutput.ToString( ));
  sWriter.Flush( );

The last step is to redirect to another page. This is necessary because allowing the page to be displayed would result in an additional rendering and an exception being thrown indicating the page has more than one server-side form element. If you need the page to be displayable anyway, a parameter can be passed in the querystring and checked in the code to determine if the output should be rendered and written to a file or handled normally.


VB
  Response.Redirect([next page])

C#
  Response.Redirect([next page]);

This technique can be used for individual controls in the same manner as for the entire page. For example, if you have a page that contains a DataGrid and you want the rendered HTML for just the DataGrid, you can call the RenderControl method of the DataGrid and then save the output as described earlier.

See Also

Recipe 18.3

Example 18-16. Capturing rendered output (.vb)

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

    Const OUTPUT_FILENAME As String = "CH18CaptureRenderedOutputVB.html"

    Dim renderedOutput As StringBuilder
    Dim strWriter As StringWriter
    Dim tWriter As HtmlTextWriter
    Dim outputStream As FileStream
    Dim sWriter As StreamWriter
    Dim filename As String
    Dim nextPage As String

    Try
      'set the names of the XML and XSLT documents used in the 
      'transformation
      xmlTransform.DocumentSource = "xml/books.xml"
      xmlTransform.TransformSource = "xml/books.xslt"

      'create a HtmlTextWriter to use for rendering the page
      renderedOutput = New StringBuilder
      strWriter = New StringWriter(renderedOutput)
      tWriter = New HtmlTextWriter(strWriter)
      'render the page output
      Page.RenderControl(tWriter)
      'save the rendered output to a file
      filename = Server.MapPath(".") & "\" & OUTPUT_FILENAME
      outputStream = New FileStream(filename, _
                                    FileMode.Create)
      sWriter = New StreamWriter(outputStream)
      sWriter.Write(renderedOutput.ToString( ))
      sWriter.Flush( )
      'redirect to another page
      'NOTE: Continuing with the display of this page will result in the 
      '      page being rendered a second time which will cause an exception 
      '      to be thrown
      nextPage = DisplayMessage.PAGE_NAME & "?" & _
                 DisplayMessage.QS_PAGE_HEADER & "=Information" & "&" & _
                 DisplayMessage.QS_MESSAGE_LINE1 & "=HTML Output Saved To " & _
                 OUTPUT_FILENAME
      Response.Redirect(nextPage)

    Finally
      'clean up
      If (Not IsNothing(outputStream)) Then
        outputStream.Close( )
      End If

      If (Not IsNothing(tWriter)) Then
        tWriter.Close( )
      End If

      If (Not IsNothing(strWriter)) Then
        strWriter.Close( )
      End If
    End Try
  End Sub  'Page_Load

Example 18-17. Capturing rendered output (.cs)

private void Page_Load(object sender, System.EventArgs e)
{
  const string OUTPUT_FILENAME = "CaptureRenderedOutput_VB.html";

  StringBuilder renderedOutput = null;
  StringWriter strWriter = null;
  HtmlTextWriter tWriter = null;
  FileStream outputStream = null;
  StreamWriter sWriter = null;
  String filename = null;
  String nextPage = null;

  try
  {
    // set the names of the XML and XSLT documents used in the 
    // transformation
    xmlTransform.DocumentSource = "xml//books.xml";
    xmlTransform.TransformSource = "xml//books.xslt";

    // create a HtmlTextWriter to use for rendering the page
    renderedOutput = new StringBuilder( );
    strWriter = new StringWriter(renderedOutput);
    tWriter = new HtmlTextWriter(strWriter);
    // render the page output
    Page.RenderControl(tWriter);
    // save the rendered output to a file
    filename = Server.MapPath(".") + "\\" + OUTPUT_FILENAME;
    outputStream = new FileStream(filename,
                                  FileMode.Create);
    sWriter = new StreamWriter(outputStream);
    sWriter.Write(renderedOutput.ToString( ));
    sWriter.Flush( );
    // redirect to another page
    // NOTE: Continuing with the display of this page will result in the 
    //       page being rendered a second time which will cause an exception 
    //       to be thrown
    nextPage = DisplayMessage.PAGE_NAME + "?" +
               DisplayMessage.QS_PAGE_HEADER + "=Information" + "&" +
               DisplayMessage.QS_MESSAGE_LINE1 + "=HTML Output Saved To " +
               OUTPUT_FILENAME;
    Response.Redirect(nextPage);
  }

  finally
  {
    // clean up
    if (outputStream != null)
    {
      outputStream.Close( );
    }

    if (tWriter != null)
    {
      tWriter.Close( );
    }

    if (strWriter != null) 
    {
      strWriter.Close( );
    }
  }
}  // Page_Load

Mikkel Aaland is a professional photographer whose pioneering work in digital photography dates back to 1981. He is the author of nine books including the bestselling Photoshop Elements Solutions and O'Reilly's acclaimed Photoshop Lightroom Adventure. Visit his website at http://www.shooting-digital.com.

Geoffrey T. LeBlond


Return to ONDotnet.com

Copyright © 2009 O'Reilly Media, Inc.