Creating a Custom BuildProvider
When
you write an ASP.NET page and save the page to your computer’s file
system, the ASP.NET page gets compiled dynamically into a .NET class in
the background. The page is compiled dynamically by a BuildProvider. The
ASP.NET Framework includes a number of BuildProviders. Each
BuildProvider is responsible for compiling a file with a particular
extension that is located in a particular type of folder. For example,
there are BuildProviders for Themes, Master Pages, User Controls, and
Web Services. When a BuildProvider builds, it builds a new class in the
Temporary ASP.NET Files folder. Any class added to the folder becomes
available to your application automatically. When you use Visual Web
Developer, any public properties and methods of the class appear in
Intellisense.
You can
create your own BuildProviders. This can be useful in a variety of
different scenarios. For example, imagine that you find yourself
building a lot of ASP.NET pages that display forms. You can tediously
build each ASP.NET page by hand by adding all the necessary form and
validation controls. Alternatively, you can create a new BuildProvider
that takes an XML file and generates the form pages for you
automatically.
Creating a Simple BuildProvider
Let’s
start by creating a really simple BuildProvider. The new BuildProvider
will be named the SimpleBuildProvider. Whenever you create a file that
has the extension .simple, the SimpleBuilderProvider builds a new class
with the same name as the file in the background. The dynamically
compiled class also includes a single method named DoSomething() that
doesn’t actually do anything.
LISTING App_Code\CustomBuildProviders\SimpleBuildProvider.cs
using System;
using System.Web.Compilation;
using System.CodeDom;
using System.IO;
namespace AspNetUnleashed
{
public class SimpleBuildProvider : BuildProvider
{
public override void GenerateCode(AssemblyBuilder ab)
{
string fileName = Path.GetFileNameWithoutExtension(this.VirtualPath);
string snippet = “public class “ + fileName + @”
{
public static void DoSomething(){}
}”;
ab.AddCodeCompileUnit(this, new CodeSnippetCompileUnit(snippet));
}
}
}
All
BuildProviders must inherit from the base BuildProvider class.
Typically, you override the BuildProvider class GenerateCode() method.
This method is responsible for generating the class that gets added to
the Temporary ASP.NET Files folder. An instance of the AssemblyBuilder
class is passed to the GenerateCode() method. You add the class that you
want to create to this AssemblyBuilder by calling the
AssemblyBuilder.AddCodeCompileUnit() method.
In Listing,
a CodeSnippetCompileUnit is used to represent the source code for the
class. Any code that you represent with the CodeSnippetCompileUnit is
added, verbatim, to the dynamically generated class. This approach is
problematic. Unfortunately, you can use the SimpleBuildProvider in
Listing only when building a C# application. It doesn’t work with a
Visual Basic .NET application. Because the code represented by the
CodeSnippetCompileUnit is C# code, using the SimpleBuildProvider with a
Visual Basic .NET application would result in compilation errors. The
SimpleBuildProvider would inject C# code into a Visual Basic .NET
assembly. The proper way to write the SimpleBuildProvider class would be
to use the CodeDom. The CodeDom enables you to represent .NET code in a
language neutral manner. When you represent a block of code with the
CodeDom, the code can be converted to either C# or Visual Basic .NET
code automatically. You learn how to use the CodeDom when we build a
more complicated BuildProvider in the next section. For now, just
realize that we are taking a shortcut to keep things simple.
When you
add the SimpleBuildProvider to your project, it is important that you
add the file to a separate subfolder in your App_Code folder and you
mark the folder as a separate code folder in the web configuration file.
For example, in the sample code on the CD that accompanies this book,
the SimpleBuildProvider is located in the App_Code\ CustomBuildProviders
folder. You must add a BuildProvider to a separate subfolder because a
BuildProvider must be compiled into a different assembly than the other
code in the App_Code folder. This makes sense because a BuildProvider is
actually responsible for compiling the other code in the App_Code
folder.
The web configuration file in Listing defines the CustomBuildProviders folder and registers the SimpleBuildProvider.
LISTING Web.Config
<configuration>
<system.web>
<compilation>
<codeSubDirectories>
<add directoryName=”CustomBuildProviders”/>
</codeSubDirectories>
<buildProviders>
<add extension=”.simple” type=”AspNetUnleashed.SimpleBuildProvider” />
</buildProviders>
</compilation>
</system.web>
</configuration>
The web
configuration file in Listing associates the SimpleBuildProvider with
the file extension .simple. Whenever you add a file with the .simple
extension to the App_Code folder, the SimpleBuildProvider automatically
compiles a new class based on the file. Build Providers execute at
different times depending on the type of folder. Build Providers
associated with the App_Code folder execute immediately after a new file
is saved. (Oddly, the Build Provider executes twice.) Build Providers
associated with the Web or App_Data folders execute when a file is
requested. For example, adding the file in Listing to your App_Code
folder causes the SimpleBuildProvider to create a new class named Mike.
LISTING App_Code\Mike.simple
Hello!
Hello!
Hello!
The actual
content of the file that you create doesn’t matter. The
SimpleBuildProvider ignores everything about the file except for the
name of the file. You can see the new file created by the
SimpleBuildProvider by navigating to the Sources_App_Code folder
contained in the folder that corresponds to your application in the
Temporary ASP.NET Files folder. The contents of the auto-generated file
are contained in Listing.
LISTING mike.simple.72cecc2a.cs
#pragma checksum “C:\Chapter27\Code\CS\App_Code\Mike.simple” “{406ea660-64cf-4c82-
b6f0-42d48172a799}” “AD2E00BE337DD88E4E4B07F6B4580617”
public class Mike
{
public static void DoSomething(){}
}
Any class added to the Temporary ASP.NET Files folder is available in your application
automatically. For example, the page in Listing 27.5 uses the Mike class.
LISTING 27.5 ShowSimpleBuildProvider.aspx
<%@ Page Language=”C#” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
“http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<script runat=”server”>
void Page_Load()
{
Mike.DoSomething();
}
</script>
<html xmlns=”http://www.w3.org/1999/xhtml” >
<head id=”Head1” runat=”server”>
<title>Show SimpleBuildProvider</title>
</head>
<body>
<form id=”form1” runat=”server”>
<div>
</div>
</form>
</body>
</html>
Creating a Data Access Component BuildProvider
In
the previous section, we created a simple but useless BuildProvider. In
this section, we create a complicated but useful BuildProvider. The
DataBuildProvider generates a data access component automatically from
an XML file. For example, if you add the XML file in Listing to your
project, then the DataBuildProvider generates the class in Listing
automatically.
LISTING App_Code\Movie.data
<Movies>
<add name=”Title” />
<add name=”Director” />
<add name=”BoxOfficeTotals” type=”Decimal” />
</Movies>
#pragma checksum “C:\Documents and Settings\Steve\My Documents\ASP.NET 3.5
Unleashed\Chapter27\Code\CS\App_Code\Movie.data” “{406ea660-64cf-4c82-b6f0-
42d48172a799}” “2E0F31E6B8F9D4B8687F94F0305E6D15”
//———————————————————————————————————————
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:2.0.50727.1378
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//———————————————————————————————————————
namespace Data {
using System;
public partial class Movie {
private string _Title;
private string _Director;
private Decimal _BoxOfficeTotals;
public Movie() {
}
public virtual string Title {
get {
return this._Title;
}
set {
this._Title = value;
}
}
public virtual string Director {
get {
return this._Director;
}
set {
this._Director = value;
}
}
public virtual Decimal BoxOfficeTotals {
1410
LISTING 27.7 Continued
get {
return this._BoxOfficeTotals;
}
set {
this._BoxOfficeTotals = value;
}
}
/// <summary>Returns List of Movie</summary>
public static System.Collections.Generic.List<Movie>
➥Select(System.Data.SqlClient.SqlConnection con) {
System.Collections.Generic.List<Movie> results = new
➥System.Collections.Generic.List<Movie>();
System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient.
➥SqlCommand();
cmd.Connection = con;
string cmdText = “SELECT Title,Director,BoxOfficeTotals FROM Movies”;
cmd.CommandText = cmdText;
System.Data.SqlClient.SqlDataReader reader = cmd.ExecuteReader();
int counter;
for (counter = 0; reader.Read(); counter = (counter + 1)) {
Movie record = new Movie();
record.Title = ((string)(reader[“Title”]));
record.Director = ((string)(reader[“Director”]));
record.BoxOfficeTotals = ((Decimal)(reader[“BoxOfficeTotals”]));
results.Add(record);
}
return results;
}
/// <summary>Returns List of Movie</summary>
public static System.Collections.Generic.List<Movie> Select(string
➥connectionStringName) {
System.Collections.Generic.List<Movie> results = new
➥System.Collections.Generic.List<Movie>();
System.Configuration.ConnectionStringSettings conStringSettings =
➥System.Web.Configuration.WebConfigurationManager.ConnectionStrings
➥[connectionStringName];
string conString = conStringSettings.ConnectionString;
System.Data.SqlClient.SqlConnection con = new
➥System.Data.SqlClient.SqlConnection();
con.ConnectionString = conString;
try {
con.Open();
results = Movie.Select(con);
}
finally {
con.Close();
}
return results;
}
}
}
LISTING ShowDataBuildProvider.aspx
<%@ Page Language=”C#” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
“http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<script runat=”server”>
void Page_Load()
{
grdMovies.DataSource = Data.Movie.Select(“Movies”);
grdMovies.DataBind();
}
</script>
<html xmlns=”http://www.w3.org/1999/xhtml” >
<head id=”Head1” runat=”server”>
<title>Untitled Page</title>
</head>
<body>
<form id=”form1” runat=”server”>
<div>
<asp:GridView
id=”grdMovies”
Runat=”server” />
</div>
</form>
</body>
</html>
Unlike the
SimpleBuildProvider created in the previous section, the
DataBuildProvider uses the CodeDom to represent code. This means that
you can use the DataBuildProvider in both Visual Basic .NET and C#
applications. The DataBuildProvider generates the data access component
in different languages automatically. For example, if you use the
DataBuildProvider in a C# application, the BuildProvider generates the
code in Listing in C#.
LISTING 27.9 DataBuildProvider.cs (Partial)
using System;
using System.Collections.Generic;
using System.Web.Compilation;
using System.CodeDom;
using System.Xml;
using System.IO;
using System.Web.Hosting;
namespace AspNetUnleashed
{
public class DataBuildProvider : BuildProvider
{
string _className;
public override void GenerateCode(AssemblyBuilder ab)
{
// Load the XML file
XmlDocument xmlData = new XmlDocument();
xmlData.Load(HostingEnvironment.MapPath(this.VirtualPath));
// Generate code from XML document
CodeCompileUnit dataCode = GetDataCode(xmlData);
// Add the code
ab.AddCodeCompileUnit(this, dataCode);
}
private CodeCompileUnit GetDataCode(XmlDocument xmlData)
{
// Add class
_className = Path.GetFileNameWithoutExtension(this.VirtualPath);
CodeTypeDeclaration dataType = new CodeTypeDeclaration(_className);
dataType.IsPartial = true;
// Add constructor
AddConstructor(dataType);
// Add properties
AddProperties(dataType, xmlData);
// Add Select method
AddSelect(dataType, xmlData);
// Add Select with conString overload
1414
LISTING 27.9 Continued
AddSelectConString(dataType, xmlData);
// Create namespace
CodeNamespace dataNS = new CodeNamespace(“Data”);
// Add class to namespace
dataNS.Types.Add(dataType);
// Create code unit
CodeCompileUnit dataCode = new CodeCompileUnit();
// Add namespace to code unit
dataCode.Namespaces.Add(dataNS);
// Add default namespaces
dataNS.Imports.Add(new CodeNamespaceImport(“System”));
return dataCode;
}
}
}
Creating a Custom ExpressionBuilder
An
ExpressionBuilder class generates one expression from another
expression. Typically, you use an ExpressionBuilder to look up a
particular value given a particular key. The ASP.NET Framework includes
the following ExpressionBuilder classes:
AppSettingsExpressionBuilder—Retrieves values from the appSettings section of the web configuration file.
ConnectionStringsExpressionBuilder—Retrieves values from the connectionStrings section of the web configuration file.
ResourceExpressionBuilder—Retrieves values from resource files.
The
ConnectionStringsExpressionBuilder has been used throughout this book
whenever a connection string has needed to be retrieved. You use the
following syntax when working with an ExpressionBuilder:
<%$ ConnectionStrings:MyDatabase %>
The <%$
and %> tags are used to mark an expression that should be parsed by
an ExpressionBuilder. The prefix ConnectionStrings is mapped to the
particular ExpressionBuilder class that is responsible for parsing the
expression. ExpressionBuilders must always be used with control
properties. For example, you cannot display a connection string in a
page like this:
<%$ ConnectionStrings:MyDatabase %>
Instead, you must display the connection string like this:
<asp:Literal Id=”ltlConnectionString” Text=’<%$ ConnectionStrings:MyDatabase %>’ Runat=”server” />
You can
create a custom ExpressionBuilder when none of the existing
ExpressionBuilder classes do what you need. For example, you might want
to store your application settings in a custom section of the web
configuration file. In that case, you might want to create a custom
ExpressionBuilder that grabs values from the custom configuration
section.
LISTING App_Code\LookupExpressionBuilder.cs
using System;
using System.CodeDom;
using System.Web.UI;
using System.ComponentModel;
using System.Web.Compilation;
using System.Xml;
using System.Web.Hosting;
using System.Web.Caching;
namespace AspNetUnleashed
{
public class LookupExpressionBuilder : ExpressionBuilder
{
public override CodeExpression GetCodeExpression(BoundPropertyEntry entry,
➥object parsedData, ExpressionBuilderContext context)
{
CodeTypeReferenceExpression refMe = new
➥CodeTypeReferenceExpression(base.GetType());
CodePrimitiveExpression expression = new
➥CodePrimitiveExpression(entry.Expression);
return new CodeMethodInvokeExpression(refMe, “GetEvalData”, new
➥CodeExpression[] { expression });
}
public override object EvaluateExpression(object target, BoundPropertyEntry
➥entry, object parsedData, ExpressionBuilderContext context)
{
return GetEvalData(entry.Expression);
}
public override bool SupportsEvaluate
{
get
{
return true;
CHAPTER 27 Working with the HTTP Runtime
1417
27
Creating a Custom ExpressionBuilder
}
}
public static string GetEvalData(string expression)
{
XmlDocument lookupDoc =
➥(XmlDocument)HostingEnvironment.Cache[“Lookup”];
if (lookupDoc == null)
{
lookupDoc = new XmlDocument();
string lookupFileName = HostingEnvironment.MapPath
➥(“~/Lookup.config”);
lookupDoc.Load(lookupFileName);
CacheDependency fileDepend = new CacheDependency(lookupFileName);
HostingEnvironment.Cache.Insert(“Lookup”, lookupDoc, fileDepend);
}
string search = String.Format(“//add[@key=’{0}’]”, expression);
XmlNode match = lookupDoc.SelectSingleNode(search);
if (match != null)
return match.Attributes[“value”].Value;
return “[no match]”;
}
}
}
Before you
can use the LookupExpressionBuilder class, you need to register it in
the web configuration file. The web configuration file in Listing
includes an <expressionBuilders> section that registers the
LookupExpressionBuilder class for the prefix lookup.
LISTING Web.Config
<configuration>
<system.web>
<compilation>
<expressionBuilders>
<add expressionPrefix=”lookup” type=”AspNetUnleashed.LookupExpressionBuilder” />
</expressionBuilders>
</compilation>
</system.web>
</configuration>
The
LookupExpressionBuilder uses an XML file named Lookup.config to contain a
database of lookup values. This file contains key and value pairs. A
sample Lookup.config file is contained in Listing 27.12.
LISTING Lookup.config
<lookup>
<add key=”WelcomeMessage” value=”Welcome to our Web site!” />
<add key=”Copyright” value=”All content copyrighted by the company.” />
</lookup>
Finally,
the page in Listing.13 uses the LookupExpressionBuilder. It contains a
Literal control that displays the value of a lookup expression named
WelcomeMessage.
LISTING ShowLookupExpressionBuilder.aspx
<%@ Page Language=”C#” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
“http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml” >
<head id=”Head1” runat=”server”>
<title>Show LookupExpressionBuilder</title>
</head>
<body>
<form id=”form1” runat=”server”>
<div>
<asp:Literal ID=”Literal1” Text=”<%$ lookup:WelcomeMessage %>” runat=”Server” />
</div>
</form>
</body>
</html>
You create a custom ExpressionBuilder by inheriting a new class from the base ExpressionBuilder class. The ExpressionBuilder class has the following methods:
GetCodeExpression—Returns the code that is used to evaluate an expression.
EvaluateExpression—Evaluates the expression in the case of no-compile ASP.NET pages.
ParseExpression—Returns a parsed version of the expression.
The ExpressionBuilder class also supports the following property:
SupportsEvaluate—When true, the ExpressionBuilder can be used in no-compile ASP.NET pages.
Creating HTTP Handlers
An
HTTP Handler is a .NET class that executes whenever you make a request
for a file at a certain path. Each type of resource that you can request
from an ASP.NET application has a corresponding handler. For example,
when you request an ASP.NET page, the Page class executes. The Page
class is actually an HTTP Handler because it implements the IHttpHandler
interface. Other examples of HTTP Handlers are the TraceHandler class,
which displays applicationlevel trace information when you request the
Trace.axd page and the ForbiddenHandler class, which displays an Access
Forbidden message when you attempt to request source code files from the
browser.
You can
implement your own HTTP handlers. For example, imagine that you want to
store all your images in a database table. However, you want use normal
HTML <img> tags to display images in your web pages. In that case,
you can map any file that has a .gif or .jpeg extension to a custom
image HTTP handler. The image HTTP handler can retrieve images from a
database automatically whenever an image request is made. Or imagine
that you want to expose an RSS feed from your website. In that case, you
can create a RSS HTTP Handler that displays a list of blog entries or
articles hosted on your website.
You can create an HTTP Handler in two ways. You can either create something called a Generic
Handler, or you can implement the IHttpHandler interface in a custom
class. This section explores both methods of creating an HTTP Handler.
Creating a Generic Handler
The
easiest way to create a new HTTP Handler is to create a Generic
Handler. When you create a Generic Handler, you create a file that ends
with the extension .ashx. Whenever you request the .ashx file, the
Generic Handler executes. You can think of a Generic Handler as a very
lightweight ASP.NET page. A Generic Handler is like an ASP.NET page that
contains a single method that renders content to the browser. You can’t
add any controls declaratively to a Generic Handler. A Generic Handler
also doesn’t support events such as the Page Load or Page PreRender
events.
LISTING ImageTextHandler.ashx
<%@ WebHandler Language=”C#” Class=”ImageTextHandler” %>
using System;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;
public class ImageTextHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
// Get parameters from querystring
string text = context.Request.QueryString[“text”];
string font = context.Request.QueryString[“font”];
string size = context.Request.QueryString[“size”];
// Create Font
Font fntText = new Font(font, float.Parse(size));
// Calculate image width and height
Bitmap bmp = new Bitmap(10, 10);
Graphics g = Graphics.FromImage(bmp);
SizeF bmpSize = g.MeasureString(text, fntText);
int width = (int)Math.Ceiling(bmpSize.Width);
int height = (int)Math.Ceiling(bmpSize.Height);
bmp = new Bitmap(bmp, width, height);
g.Dispose();
// Draw the text
g = Graphics.FromImage(bmp);
g.Clear(Color.White);
g.DrawString(text, fntText, Brushes.Black, new PointF(0, 0));
g.Dispose();
1422 CHAPTER 27 Working with the HTTP Runtime
LISTING 27.14 Continued
// Save bitmap to output stream
bmp.Save(context.Response.OutputStream, ImageFormat.Gif);
}
public bool IsReusable
{
get
{
return true;
}
}
}
The
ImageTextHandler in Listing includes one method and one property. The
ProcessRequest() method is responsible for outputting any content that
the handler renders to the browser. In Listing, the image text, font,
and size are retrieved from query string fields. You specify the image
that you want to return from the handler by making a request that looks
like this:
/ImageTextHandler.ashx?text=Hello&font=Arial&size=30
Next, a
bitmap is created with the help of the classes from the System.Drawing
namespace. The bitmap is actually created twice. The first one is used
to measure the size of the bitmap required for generating an image that
contains the text. Next, a new bitmap of the correct size is created,
and the text is drawn on the bitmap. After the bitmap has been created,
it is saved to the HttpResponse object’s OutputStream so that it can be
rendered to the browser. The
handler in Listing also includes an IsReusable property. The IsReusable
property indicates whether the same handler can be reused over multiple
requests. You can improve your application’s performance by returning
the value True. Because the handler isn’t maintaining any state
information, there is nothing wrong with releasing it back into the pool
so that it can be used with a future request. The page in Listing
illustrates how you can use the ImageTextHandler.ashx file.
This page contains three HTML <img> tags that pass different query strings to the handler
LISTING ShowImageTextHandler.aspx
<%@ Page Language=”C#” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
“http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml” >
<head id=”Head1” runat=”server”>
<title>Show ImageTextHandler</title>
</head>
<body>
<form id=”form1” runat=”server”>
<div>
<img src=”ImageTextHandler.ashx?text=Some Text&font=WebDings&size=42” />
<br />
<img src=”ImageTextHandler.ashx?text=Some Text&font=Comic Sans MS&size=42” />
<br />
<img src=”ImageTextHandler.ashx?text=Some Text&font=Courier New&size=42” />
</div>
</form>
</body>
</html>
Implementing the IHttpHandler Interface
The
big disadvantage of a Generic Handler is that you cannot map a Generic
Handler to a particular page path. For example, you cannot execute a
Generic Handler whenever someone requests a file with the extension
.gif. If you need more control over when an HTTP Handler executes, then
you can create a class that implements the IHttpHandler interface. After
you create a class that For example, the class in Listing represents an
Image HTTP Handler. This handler retrieves an image from a database
table and renders the image to the browser.
LISTING App_Code\ImageHandler.cs
using System;
using System.Web;
using System.Data;
using System.Data.SqlClient;
using System.Web.Configuration;
namespace AspNetUnleashed
{
public class ImageHandler : IHttpHandler
{
const string connectionStringName = “Images”;
public void ProcessRequest(HttpContext context)
{
// Don’t buffer response
context.Response.Buffer = false;
// Get file name
string fileName = VirtualPathUtility.GetFileName(context.Request.Path);
// Get image from database
string conString = WebConfigurationManager.ConnectionStrings
➥[connectionStringName].ConnectionString;
SqlConnection con = new SqlConnection(conString);
SqlCommand cmd = new SqlCommand(“SELECT Image FROM Images WHERE
➥FileName=@FileName”, con);
cmd.Parameters.AddWithValue(“@fileName”, fileName);
using (con)
{
con.Open();
SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.
➥SequentialAccess);
if (reader.Read())
{
int bufferSize = 8040;
byte[] chunk = new byte[bufferSize];
long retCount;
long startIndex = 0;
retCount = reader.GetBytes(0, startIndex, chunk, 0, bufferSize);
while (retCount == bufferSize)
{
context.Response.BinaryWrite(chunk);
startIndex += bufferSize;
retCount = reader.GetBytes(0, startIndex, chunk, 0,
➥bufferSize);
}
byte[] actualChunk = new Byte[retCount - 1];
Buffer.BlockCopy(chunk, 0, actualChunk, 0, (int)retCount - 1);
context.Response.BinaryWrite(actualChunk);
}
}
}
public bool IsReusable
{
get { return true; }
}
}
}
implements
the IHttpHandler interface, you need to register the class in the web
configuration file. The web configuration file in Listing includes an
httpHandlers section that associates the .gif, .jpeg, and .jpg
extensions with the Image handler.
LISTING Web.Config
<configuration>
<connectionStrings>
<add name=”Images” connectionString=”Data Source=.\SQLExpress;Integrated
Security=True;AttachDBFileName=|DataDirectory|ImagesDB.mdf; User Instance=True”/>
</connectionStrings>
<system.web>
<httpHandlers>
<add path=”*.gif” verb=”*”
type=”AspNetUnleashed.ImageHandler” validate=”false” />
<add path=”*.jpeg” verb=”*”
type=”AspNetUnleashed.ImageHandler” validate=”false” />
<add path=”*.jpg” verb=”*”
1426 CHAPTER 27 Working with the HTTP Runtime
LISTING 27.17 Continued
type=”AspNetUnleashed.ImageHandler” validate=”false” />
</httpHandlers>
</system.web>
</configuration>
When you register a handler, you specify the following four attributes:
path—Enables you to specify the path associated with the handler. You can use wildcards in the path expression.
verb—Enables
you to specify the HTTP verbs, such as GET or POST, associated with the
handler. You can specify multiple verbs in a comma-separated list. You
can represent any verb with the * wildcard.
type—Enables you to specify the name of the class that implements the handler.
validate—Enables
you to specify whether the handler is loaded during application
startup. When true, the handler is loaded at startup. When false, the
handler is not loaded until a request associated with the handler is
made. This second option can improve your application’s performance when
a handler is never used.
The page in
Listinguses the ImageHandler to render its images. The page enables you
to upload new images to a database named ImagesDB. The page also
displays existing images
<%@ Page Language=”C#” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
“http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<script runat=”server”>
protected void btnAdd_Click(object sender, EventArgs e)
{
if (upFile.HasFile)
{
srcImages.Insert();
}
}
</script>
<html xmlns=”http://www.w3.org/1999/xhtml” >
<head id=”Head1” runat=”server”>
<style type=”text/css”>
.fileList li
{
margin-bottom:5px;
}
</style>
<title>Image Upload</title>
</head>
<body>
<form id=”form1” runat=”server”>
<div>
<asp:Label id=”lblFile” Text=”Image File:” AssociatedControlID=”upFile” Runat=”server” />
<asp:FileUpload id=”upFile” Runat=”server” />
<asp:Button id=”btnAdd” Text=”Add Image” OnClick=”btnAdd_Click” Runat=”server” />
<hr />
<asp:GridView id=”grdImages” DataSourceID=”srcImages” AutoGenerateColumns=”false”
ShowHeader=”false” GridLines=”None” Runat=”server”>
<Columns>
<asp:ImageField DataImageUrlField=”FileName” DataAlternateTextField=”FileName” />
</Columns>
</asp:GridView>
<asp:SqlDataSource id=”srcImages” ConnectionString=”<%$ ConnectionStrings:Images %>”
SelectCommand=”SELECT
FileName FROM Images” InsertCommand=”INSERT Images (FileName,Image)
VALUES (@FileName,@FileBytes)” Runat=”server”>
<InsertParameters>
<asp:ControlParameter Name=”FileName” ControlID=”upFile” ➥PropertyName=”FileName” />
<asp:ControlParameter Name=”FileBytes” ControlID=”upFile” ➥PropertyName=”FileBytes” />
</InsertParameters>
</asp:SqlDataSource>
</div>
</form>
</body>
</html>
Registering Extensions with Internet Information Server
The
web server included with Visual Web Developer maps all requests to the
ASP.NET Framework. For example, if you create an HTTP Handler that
handles requests for .gif files, then you don’t have to do anything
special when using the handler with the Visual Web Developer web server.
Internet Information Server, on the other hand, does not map all
requests to the ASP.NET Framework. In particular, it does not map
requests for .gif files to ASP.NET. If you want to use a special
extension for a handler, then you must configure Internet Information
Server to map that extension to the ASP.NET Framework. If you are
serving your pages with Internet Information Server 6.0 (included with
Windows Server 2003), then you can create something called a wildcard
application mapping. A wildcard application mapping enables you to map
all page requests to an application such as the ASP.NET Framework.
Follow these steps to configure a wildcard mapping for ASP.NET:
1. Open Internet Information Services by selecting Start, Control Panel, Administrative Tools, Internet Information Services.
2. Open the property sheet for a particular website or virtual directory.
3. Open the Application Configuration dialog box by selecting the Directory tab and clicking the Configuration button.
4. Select the Mappings tab.
5. Click the Insert button at the bottom of the Mappings tab to open the Add/Edit Application Extension Mapping dialog box .
6.
In the Executable field, enter the path to the ASP.NET ISAPI DLL. (You
can copy and paste this path from the Application Mapping for the .aspx
extension.)
After you
complete these steps, all requests made for any type of file are handled
by the ASP.NET Framework. If you make a request for a .gif image, then
any handlers that you have registered in the web configuration file for
the .gif extension will execute. Earlier versions of Internet
Information Server, such as the version included with Microsoft Windows
XP, do not support wildcard application mappings. You must map each file
extension that you want to associate with the ASP.NET Framework one by
one.
Follow these steps to map the .gif extension to the ASP.NET Framework:
1. Open Internet Information Services by selecting Start, Control Panel, Administrative Tools, Internet Information Services.
2. Open the property sheet for a particular website or virtual directory.
3. Open the Application Configuration dialog box by selecting the Directory tab and clicking the Configuration button.
4. Select the Mappings tab.
5. Click the Add button to open the Add/Edit Application Extension Mapping dialog box.
6.
In the Executable field, enter the path to the ASP.NET ISAPI DLL. (You
can copy and paste this path from the Application Mapping for the .aspx
extension.)
7. In the Extension field, enter .gif.
After you
complete these steps, requests for .gif images are handled by the
ASP.NET Framework. If you have registered an HTTP handler for the .gif
extension in the web configuration file, then the HTTP Handler will
execute whenever someone makes a request for a .gif file.
Creating an Asynchronous HTTP Handler
When
you create an HTTP Handler by creating either a Generic Handler or
implementing the IHttpHandler interface, you are creating a synchronous
handler. In this section, you learn how to create an asynchronous
handler. The advantage of creating an asynchronous handler is
scalability. The ASP.NET Framework maintains a limited pool of threads
that are used to service requests. When the ASP.NET Framework receives a
request for a file, it assigns a thread to handle the request. If the
ASP.NET Framework runs out of threads, the request is queued until a
thread becomes available. If too many threads are queued, then the
framework rejects the page request with a 503 Server Too Busy response
code. If you execute an HTTP Handler asynchronously, then the current
thread is released back into the thread pool so that it can be used to
service another page request. While the asynchronous handler is
executing, the ASP.NET framework can devote its attention to handling
other requests. When the asynchronous handler completes its work, the
framework reassigns a thread to the original request and the handler can
render content to the browser.
You can
configure the ASP.NET thread pool with the httpRuntime element in the
web configuration file. You can modify the appRequestQueueLimit,
minFreeThreads, and minLocalRequestFreeThreads attributes to control how
many requests the ASP.NET Framework queues before giving up and sending
an error. You create an asynchronous HTTP handler by implementing the
IHttpAsyncHandler interface. This interface derives from the
IHttpHandler interface and adds two additional methods:
BeginProcessRequest—Called to start the asynchronous task.
EndProcessRequest—Called when the asynchronous task completes.
Working with HTTP Applications and HTTP Modules
Whenever
you request an ASP.NET page, the ASP.NET Framework assigns an instance
of the HttpApplication class to the request. This class performs the
following actions in the following order:
1. Raises the BeginRequest event.
2. Raises the AuthenticateRequest event.
3. Raises the AuthorizeRequest event.
4. Calls the ProcessRequest() method of the Page class.
5. Raises the EndRequest event.
This is not
a complete list of HttpApplication events. There are a lot of them! The
entire page execution lifecycle happens during the fourth step. For
example, the Page Init, Load, and PreRender events all happen when the
Page class ProcessRequest() method is called. The HttpApplication object
is responsible for raising application events. These application events
happen both before and after a page is executed.
If you want
to handle HttpApplication events, there are two ways to do it. You can
create a Global.asax file, or you can create one or more custom HTTP
Modules.
Creating a Global.asax File
By
default, the ASP.NET Framework maintains a pool of HttpApplication
objects to service incoming page requests. A separate HttpApplication
instance is assigned to each request. If you prefer, you can create a
custom HttpApplication class. That way, an instance of your custom class
is assigned to each page request. You can create custom properties in
your derived class. These properties can be accessed
from any page, control, or component. You also can handle any application events in your custom HttpApplication class.
Listing Global.asax
<%@ Application Language=”C#” %>
<%@ Import Namespace=”System.Data” %>
<%@ Import Namespace=”System.Data.SqlClient” %>
<%@ Import Namespace=”System.Web.Configuration” %>
<script runat=”server”>
private string _conString;
private SqlConnection _con;
private SqlCommand _cmdSelect;
private SqlCommand _cmdInsert;
public override void Init()
{
// initialize connection
_conString = WebConfigurationManager.ConnectionStrings[“Log”].
➥ConnectionString;
_con = new SqlConnection(_conString);
// initialize select command
_cmdSelect = new SqlCommand(“SELECT COUNT(*) FROM Log WHERE Path=@Path”,
➥_con);
_cmdSelect.Parameters.Add(“@Path”, SqlDbType.NVarChar, 500);
// initialize insert command
_cmdInsert = new SqlCommand(“INSERT Log (Path) VALUES (@Path)”, _con);
_cmdInsert.Parameters.Add(“@Path”, SqlDbType.NVarChar, 500);
}
public int NumberOfRequests
{
get
{
int result = 0;
_cmdSelect.Parameters[“@Path”].Value = Request.
➥AppRelativeCurrentExecutionFilePath;
try
{
_con.Open();
result = (int)_cmdSelect.ExecuteScalar();
}
finally
{
_con.Close();
}
return result;
}
}
void Application_BeginRequest(object sender, EventArgs e)
{
// Record new request
_cmdInsert.Parameters[“@Path”].Value = Request.
➥AppRelativeCurrentExecutionFilePath;
try
{
_con.Open();
_cmdInsert.ExecuteNonQuery();
}
finally
{
_con.Close();
}
}
</script>
Creating Custom HTTP Modules
An
HTTP Module is a .NET class that executes with each and every page
request. You can use an HTTP Module to handle any of the HttpApplication
events that you can handle in the Global.asax file. Behind the scenes,
the ASP.NET Framework uses HTTP Modules to implement many of the
standard features of the framework. For example, the ASP.NET Framework
uses the FormsAuthenticationModule to implement Forms authentication and
the WindowsAuthenticationModule to implement Windows authentication.
Session state is implemented with an HTTP Module named the
SessionStateModule. Page output caching is implemented with an HTTP
Module named the OutputCacheModule, and the Profile object is
implemented with an HTTP Module named the ProfileModule. When a new
instance of an HttpApplication class is created, the HttpApplication
loads all of the HTTP Modules configured in the web configuration file.
Each HTTP Module subscribes to one or more HttpApplication events. For
example, when the HttpApplication object raises its AuthenticateRequest
event, the FormsAuthenticationModule executes its code to authenticate
the current user.