Table of Contents
Introduction
As developers, we are often constrained by our environment (e.g., physical and virtual memory, and hard disk space), project requirements (e.g., timelines/deadlines, resources, and software quality), and customer expectations. While there is no clear 'winner' in the battle of constraints, this article focuses on the environmental constraints: specifically -- hard disk restrictions.
To help set the scene for the article, let me start with the earlier phases of the Software Development Life Cycle (SDLC) and define the problem and a few of the respective requirements.
The system that I was building needed to provide an interface for its users to upload files including images – a pretty common requirement. Another common requirement was to display the uploaded images in an image gallery-like interface that would render thumbnail images of the respective uploaded images. Yet another requirement – uncommon – was the need to minimize (as much as possible) the number of physical files on the data store to conserve hard disk space – the aforementioned hard disk constraint and problem scope.
The implementation I choose was to create an image optimizer control that would scale and optimize the quality of the original uploaded image and store the results to a data store (in this case a hard disk), and create an HTTP handler that would create thumbnail images of the optimized images on the fly, at runtime, and in memory (no physical disk space to store the temporary thumbnail file). While the image optimizer is a subject for another day; this article focuses on the development of a thumbnail creation HTTP handler – Thumbnailer HTTP handler (THH).
This article is organized into two primary modules:
- White Box Interface – the internal logic and workings of the Thumbnailer HTTP handler. This section is for those developers that are interested in the development of the Thumbnailer HTTP handler.
- Black Box Interface – how to interface with (use) the Thumbnailer HTTP handler. This section is for those developers that what to interface with the Thumbnailer HTTP handler for the functionality that it provides.
Before we jump into coding, a little background…
Background
MSDN defines an HTTP handler as: "a process (frequently referred to as the 'endpoint') that runs in response to a request made to an ASP.NET Web application. The most common handler is an ASP.NET page handler that processes .aspx files. When users request an .aspx file, the request is processed by the page via the page handler." Other common HTTP handlers are the Web service handler (*.asmx extension), ASP.NET user control handler (*.ascx extension), and the Trace handler (trace.axd).
To understand what an HTTP handler is, you should understand the processing of an ASP.NET request – commonly referred to as the ASP.NET HTTP pipeline. While a discussion of the ASP.NET pipeline could consume quite a few pages (if not a book) on its own, the diagram below briefly illustrates a common HTTP request. Once an ASP.NET resource is requested (from the client through IIS), the request passes through a series of HTTP modules and terminates at the HTTP handler. Next, the HTTP handler does its processing magic (based on the HTTP handler's logic) and produces output in the form of a response (otherwise known as the HTTP response). The response then reverses its incoming process and passes back though the HTTP modules, etc… and terminates at the client. For more information on the ASP.NET pipeline and HTTP modules, handlers, requests, and responses, please refer to the .NET SDK on MSDN or search the topic on Google.
White Box Analysis
This is not a comprehensive analysis on HTTP handlers; neither is it a 'how to' on interfaces and abstract classes, nor is it a journal on software engineering theories. However, what this article is is a focused discussion on the development of a thumbnail creation HTTP handler. Although, I will provide external links to this information, so if you are intrigued to continue your understanding of the subject(s) for professional development, you will have a handful of references to start with.
To create a custom HTTP handler, you must implement the System.Web.IHttpHandler
interface. However, once you start creating a few HTTP handlers you will immediately recognize common patterns that could be extracted to create a more universal/generic HTTP handler and use this newly create HTTP handler as the base class for your custom HTTP handlers. Well, this is exactly what Scott Hanselmann did. He created a boilerplate class that aggregates the common patterns of HTTP handlers. Then, Phillip J Haack further extended Scott's work and created an abstract HTTP handler class – BaseHttpHandler
. Object-oriented programming (OOP) is great, and when you consolidate smart minds to create reusable and extendable objects, you solidify the benefits that OOP brings to the table. Great work guys!
Needless to say, this is the class that I use to create all of my Http handlers. So let's get started…
Open Visual Studio 2005, create a new project of type 'class library', name the project anything you want (I named mine Shp.Handler
), and delete the default Class1.cs file. Now, download the BaseHttpHandler
abstract class from Phillip J Haack's site or from the source provided with this article, and add it to you your project.
If at this point you try to compile the class library, you will receive a list of errors. You need to add a reference to the System.Web
assembly, and while you're at it, add a reference to the System.Drawing
assembly, you will need this reference later. If you don't know how to add a reference to your project, please view "How to: Add or Remove References in Visual Studio" on MSDN.
Now, create a new class and name it Thumbnailer
(or anything else you want). Extend this new class from the abstract BaseHttpHandler
class and implement its abstract members. Also, add using references to the following namespaces: System.Web
, System.Web.UI
, System.Drawing
, System.Drawing.Imaging
, System.IO
, and System.Reflection.
For more information on these namespace, please refer to the .NET SDK documentation on MSDN. At this point, your class should look something like this:
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Web;
using System.Web.UI;
using System.Drawing.Imaging;
using System.Reflection;
using System.Drawing;
namespace Shp.Handler
{
class Thumbnailer : BaseHttpHandler
{
public override bool RequiresAuthentication
{
get { throw new Exception("The method or operation" +
" is not implemented."); }
}
protected override void HandleRequest (HttpContext context)
{
throw new Exception("The method or operation is not implemented.");
}
public override bool ValidateParameters (HttpContext context)
{
throw new Exception("The method or operation " +
"is not implemented.");
}
}
}
Adding comments to you code is a good, albeit often overlooked, habit to get yourself into, so in the interest of practicing good programming conventions, I've decorated the implemented members of the aforementioned code. These comments are similar to the comments from the base class, so you could have just as easily used the '<see cref="">
' comment tag to reference the respective BaseHttpHandler
abstract class member.
Because object-oriented programming in non-procedural, it is difficult (if not impossible) to explain an object's execution flow in a step-wise manner without knowledge of the respective classes' members. Therefore, below is a class diagram of the final Thumbnailer
class to assist with understanding the classes' execution flow:
The main entry point for the Thumbnailer
HTTP handler is the HandleRequest
method. This method takes an HttpContext
object as its only parameter. The HttpContext
object encapsulates all HTTP-specific information about the respective request; therefore, it provides access to the HTTPRequest
object and its members including the QueryString
collection. However, there are few properties and methods that the base class (BaseHttpHandler
) requires to be set/executed before the HandleRequest
method is invoked.
Prior to the execution of the HandleRequest
method, the base class calls/validates parameters, determines if authentication is required, and if authentication is required, checks to see if the current user is authenticated. The Thumbnailer
class is responsible for providing this information to its base via the following two overridden members:
public override bool RequiresAuthentication
{
get { return false; }
}
public override string ContentMimeType
{
get { return this._mimeText; }
}
In this case, the RequiresAuthentication
property just returns false
because the resource in question (the source image) does not require authentication. If your resource required authentication, you could easily add the necessary logic to the Thumbnailer
class or just return true. The BaseHttpHandler
class takes care of the authentication service by checking the current user's Identify object's IsAuthenticated
property. Please refer to the .NET SDK documentation on MSDN for more information.
All that the ValidateParameters
method does is return a bool
specifying if the parameters are valid (true
) or not (false
). In most cases, the HTTP handler would evaluate the incoming parameters and return false
if either one of these parameters are null or empty, otherwise return true
. However, the Thumbnailer
implementation will always return true. If the parameters are null
or empty, Thumbnailer
will return a default thumbnail image from an embedded image resource (more on this later).
Based on the successful execution of the RequiresAuthentication
and ValidateParameters
methods and additional processing (if necessary, e.g. authentication), the base class will invoke the HandleRequest
method of the Thumbnailer HTTP handler. This is where the rubber meets the road.
protected override void HandleRequest (HttpContext context)
{
if (string.IsNullOrEmpty(
context.Request.QueryString[SIZE_PARAM]))
this._sizeType = ThumbnailSizeType.Small;
else
this.SetSize(context.Request.QueryString[SIZE_PARAM]);
if ((string.IsNullOrEmpty(
context.Request.QueryString[IMG_PARAM])) ||
(!this.IsValidImage(context.Request.QueryString[IMG_PARAM])))
{
this.GetDefaultImage(context);
}
else
{
string file =
context.Request.QueryString[
IMG_PARAM].Trim().ToLower().Replace("\\", "/");
if (file.IndexOf("/") != 0)
file = "/" + file;
if (!File.Exists(context.Server.MapPath("~" + file)))
this.GetDefaultImage(context);
else
{
using (System.Drawing.Image im =
System.Drawing.Image.FromFile(
context.Server.MapPath("~" + file)))
using (System.Drawing.Image tn =
this.CreateThumbnail(im))
{
tn.Save(context.Response.OutputStream,
this._formatType);
}
}
}
}
The HandleRequest
acquires and validates the incoming parameters, generates the requested thumbnail, and return the thumbnail using the Response
object's OutputStream
property.
The Thumbnailer
acquires the following incoming parameters via the Request
object's QueryString
collection property: img
and size
. The following is a sample of the incoming request that the Thumbnailer
is registered to handle:
thumbnailer.ashx?img=im/ImageSource.jpg&size=72
As these parameters suggest, img
is the local path to the image source file and size
is an enumerated value stating the size of the requested thumbnail. When evaluated, the size parameter is transformed into a ThumbnailSizeType
enumeration:
internal enum ThumbnailSizeType
{
Small = 72,
Medium = 144,
Large = 288
}
The value associated with each enumeration name represents the maximum width and/or height (in pixels) of the requested thumbnail.
The HandleRequest
method checks to see if the size parameter exists. If it does not exist, it will set a default size of small (ThumbnailSizeType.Small
). If it does exist, HandleRequest
will invoke the SetSize
helper method.
private void SetSize (string size)
{
int sizeVal;
if (!Int32.TryParse(size.Trim(),
System.Globalization.NumberStyles.Integer,
null, out sizeVal))
sizeVal = (int)ThumbnailSizeType.Small;
try
{
this._sizeType = (ThumbnailSizeType)sizeVal;
}
catch
{
this._sizeType = ThumbnailSizeType.Small;
}
}
This method will evaluate the size parameter and set the class variable _sizeType
of type ThumbnailSize
. I use a try/catch
statement to ensure that the request contains a valid size value.
Next, the HandleRequest
method checks to see if the img
parameter is null
or empty. If the parameter is null
or empty, a default thumbnail is retrieved from the assembly as an embedded resource (more on this later), if the parameter is neither null nor empty, the IsValidImage
helper method is invoked.
private bool IsValidImage (string fileName)
{
string ext = Path.GetExtension(fileName).ToLower();
bool isValid = false;
switch (ext)
{
case ".jpg":
case ".jpeg":
isValid = true;
this._mimeText = "image/jpeg";
this._formatType = ImageFormat.Jpeg;
break;
case ".gif":
isValid = true;
this._mimeText = "image/gif";
this._formatType = ImageFormat.Jpeg;
break;
case ".png":
isValid = true;
this._mimeText = "image/png";
this._formatType = ImageFormat.Jpeg;
break;
default:
isValid = false;
break;
}
return isValid;
}
This method evaluates the incoming img
parameter based on the parameter's extension to determine the class member variable of type ImageFormat
and the overridden member ContentMimeType
. ImageFormat
is an enumeration of the System.Drawing.Imaging
namespace. It specifies the format of the image. Whereas, ContentMimeType
is a string that will ultimately determine the Response
object's ContentType
(the MIME type of the output stream). Finally, IsValidSize
will return true
or false
based on the img
parameter's validation, and if it returns false
, like the null img
parameter, the Thumbnailer
will use a default thumbnail retrieved from the assembly as an embedded image resource.
Embedded resources are commonly used by custom control designers to embed client-side scripts and images directly into the control's assembly. Embedding resources in assemblies helps maintain resource and code integrity/encapsulation on deployment. When an assembly is added to a project (e.g. Web site) that requires external files (scripts, images, etc…), embedded resources provide a one-stop approach. The developer does not have to add the required external resources to the client application as separate external files. When an assembly with embedded resources is added, the external resources are maintained and managed by the assembly itself, so issues like script registration are not necessary.
The reason why I choose to add embedded resources to the Thumbnailer
's assembly is for default image rendering. When a request is made for a resource that the Thumbnailer
is registered to service and the request does not contain the required parameters, the Thumbnailer
will render a default image embedded in its assembly.
Since an HTTP handler does not have access to the Page
object, we need to access resources directly via reflection rather than using the incredibly easy ClientScriptManager
class (via the Page.ClientScript
property).
The following code snippet illustrates the retrieval of the default image from the assembly's embedded resources:
private void GetDefaultImage (HttpContext context)
{
Assembly a = Assembly.GetAssembly(this.GetType());
Stream imgStream = null;
Bitmap bmp = null;
string file = string.Format("{0}{1}{2}",
DEFAULT_THUMBNAIL,
(int)this._sizeType, ".gif");
imgStream = a.GetManifestResourceStream(a.GetName().Name + file);
if (imgStream != null)
{
bmp = (Bitmap.FromStream(imgStream) as Bitmap);
bmp.Save(context.Response.OutputStream, this._formatType);
imgStream.Close();
bmp.Dispose();
}
}
For more information on accessing, manipulating, and retrieving resources from an assembly, please refer to the .NET SDK documentation on MSDN.
If all goes well with the incoming parameter logic, the HandleRequest
method will invoke the CreateThumbnail
method. This method takes the Image
source object as the single parameter, generates scaling variables based on the Thumbnailer
incoming size parameter, and invokes the Image
object's GetThumbnialImage
method.
private System.Drawing.Image
CreateThumbnail (System.Drawing.Image src)
{
int maxSize = (int)this._sizeType;
int w = src.Width;
int h = src.Height;
if (w > maxSize)
{
h = (h * maxSize) / w;
w = maxSize;
}
if (h > maxSize)
{
w = (w * maxSize) / h;
h = maxSize;
}
return src.GetThumbnailImage(w, h,
delegate() { return false; }, IntPtr.Zero);
}
The GetThumbnailImage
method takes four parameters: int::width
; int::height
; bool::Image.GetThumbnailImageAbort (delegate)
; and IntPtr
.
The parameter of interest here is the GetThumbnailImageAbort
. This parameter is required; however, the .NET SDK documentation states that "the delegate is not used." I'm just the messenger, not the designer :-)! GetThumbnailImageAbort
delegate's signature takes zero parameters and returns a bool
.
Why create a method matching this delegate that does nothing and pass a reference to this delegate as the third parameter of GetThumbnailImageAbort
method? I cannot answer that; however, what I can do is provide an elegant solution to implement the required delegate using .NET 2.0 Anonymous Methods.
Prior to .NET 2.0, you had to create a method that matches a delegate's signature and pass a reference to this method for use when the delegate was invoked. You can still implement it this way, but with anonymous methods we have a much more elegant solution. Rather then creating and referencing a bunch of unnecessary objects/methods, we can just pass a block of code as the delegate's parameter. This is exactly what I am doing with the following code snippet:
return src.GetThumbnailImage(w, h, delegate()
{ return false; }, IntPtr.Zero);
Note that the third parameter's signature matches the required delegate's signature: zero parameters, returning a boolen
Black Box Analysis
Whether you read the previous Thumbnailer White Box Analysis or skipped directly to this section, this section deals strictly with getting the Thumbnailer configured and into production.
- The easiest way to use Thumbnailer is download this article's code, open the Shp.Handler solution file, and compile the code. The solution consists of two projects:
- Shp.Handler: the Thumbnail HTTP handler's project
- Shp.Handler.Web: the testing Web Site project.
- If you are creating your own Web Site project, you will need to import the Shp.Handler assembly by using the 'Add Reference' utility.
- Using Shp.Handler.Web or your own Web Site, add a new Web Form.
- For this example, we will use a standard ASP.NET data
Repeater
control. Add a data Repeater control, switch to source (HTML) view, add an ItemTemplate
, and add a standard ASP.NET Image
control to the ItemTemplate
. Your code should look something like the following snippet:
<asp:Repeater ID="Repeater1" runat="server">
<ItemTemplate>
<asp:Image ID="Image1" runat="server" />
</ItemTemplate>
</asp:Repeater>
- We are going to use local images within the application's root directory, so add an image source directory within the application's root directory, and add a few JPEGs. If you are using the demo project, this directory and a few images are already present in /im/.
- To simplify the code as much as possible, we are going to use a list of source image files as the data source, rather then create a custom data source and access the data source via custom business logic. So, open the code-behind file and in the
Page
's Load
event handler add the following code:
protected void Page_Load (object sender, EventArgs e)
{
IList<FileInfo> files = new List<FileInfo>();
string filters = "*.jpg;*.png;*.gif";
foreach (string filter in filters.Split(';'))
{
FileInfo[] fit = new DirectoryInfo(
this.Server.MapPath("~/im")).GetFiles(filter);
foreach (FileInfo fi in fit)
{
files.Add(fi);
}
}
this.Repeater1.DataSource = files;
this.Repeater1.DataBind();
}
All that this method does is acquire all image files using a filter for image types, adds the images to a generic List object of type FileInfo
, and uses the List as the Repeater
's data source. Of course, don't forget to bind the Repeater
to the data source.
Note: If you are new to the FileInfo
object, take the time to look it up on MSDN. We will be using some of the FileInfo
's properties for declarative data binding.
- Switch back the source (HTML) view. For this example, we will wrap the ASP.NET
Image
control (previously added) within a standard HTML anchor control, and declaratively data bind the HTML anchor element's href
attribute and the Image
control's ImageUrl
property to the FileInfo
's Name
property. Adding a bit of style and formatting, our entire source (HTML) view will look like the following:
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="Default.aspx.cs" Inherits="_Default" %>
<!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>Thumbnailer Test</title>
<style>
.img_tn_cntr_72 {height: 84px; vertical-align:
middle; display: inline;}
.img_tn_cntr_144 {height: 156px; vertical-align:
middle; display: inline;}
.img_tn_cntr_288 {height: 300px; vertical-align:
middle; display: inline;}
.img_tn {padding: 5px; border:
1px solid #000000; margin: 5px;}
</style>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Repeater ID="Repeater1" runat="server"
OnItemDataBound="Repeater1_ItemDataBound">
<ItemTemplate>
<div class="img_tn_cntr_72">
<a href='<%# Eval("Name", "im/{0}") %>' target="_blank">
<asp:Image ID="Image1" runat="server"
ImageUrl='<%# Eval("Name", "ImageThumbnailer.ashx?img=im/{0}&size=72") %>'
CssClass="img_tn" BorderColor="black"
BorderWidth="1px" BorderStyle="solid" /></a>
</div>
</ItemTemplate>
</asp:Repeater>
</div>
</form>
</body>
</html>
Notice the Name
parameter in the in the Eval
declarative data binding statement. This is the Name
property of the FileInfo
class. Name
returns the name of the image with the file extension.
Note: Don't be concerned with the Repeater
's ItemDataBound
event. I just use this method to add custom rollover behavior. The code is easy to understand, and if you want to review it, please view the code-behind for the page in the article's download.
Take note of the following code snippet:
thumbnailer.ashx?img=im/ImageSource.jpg&size=72
This looks and acts like a standard Query String; however, rather then communicating with a Page
, this statement communicates with the Thumbnailer
HTTP handler: the client interface to the Thumbnailer
HTTP handler. thumbnailer.ashx is registered with the application in the Web.Config (see next step). The two parameters are img
(the relative path to the image source) and size
(the size of the thumbnail). The size
parameter is actually transformed into an enumeration that represents 72, 144, and 288 pixels; therefore, this parameters options are 72, 144, and 288.
- Okay, we're almost ready to build and run the application. The last step is to register the Thumbnailer HTTP handler with the application. Open up the Web.Config file and add the following as a child element to the
system.web
element:
<httpHandlers>
<add verb="*" path="ImageThumbnailer.ashx"
type="Shp.Handler.Thumbnailer, Shp.Handler"/>
</httpHandlers>
The <httpHandlers>
element 'maps incoming requests to the appropriate handler according to the URL and the HTTP verb that is specified in the request' (MSDN). In this case, the handler is Thumbnailer specified by the type
attribute and the URL is 'ImageThumbnailer.ashx' specified by the path
attribute. For more information on this and other ASP.NET configuration settings, please see the .NET SDK documentation on MSDN.
- Build and run your Web application. Your browser should render content similar to the screenshot at the beginning of this article.
Caveats
Accurately named, this section provides warnings, cautions and/or issues to look out for when using the Thumbnailer HTTP handler.
- When I was developing Thumbnailer and attempting to create thumbnails for *.bmp and *.png files and calling the
Image.Save
method, a generic GDI exception was raised. To avoid this issue, I removed support for *.bmp images and rather then create thumbnails of the same type as the source image, all thumbnails are created as *.jpg
files. Besides, for most situations where thumbnails are required, the source image will more likely then not be a *.jpg/*.jpeg file, so you may never run into similar issues.
- The included sample Web application does not render the "No Image Found" image generated by the Thumbnailer HTTP handler, because all source images are valid. However, if we were to add an invalid file/image, the "No Image Found" image would render as a default thumbnail, embedded within the Thumbnailer's assembly, for invalid files. This will be an issue within a Web application similar to the sample Web application because when the thumbnail is clicked the source image is rendered in a popup; however, in the case of the "No Image Found" image, there is no local source image. When using the Thumbnailer in this type of situation, you must handle this issue within the client Web application, otherwise ASP.NET will throw an exception.
Note: If you did not read 'White Box Analysis' and the "No Image Found" issue is unclear, please review the 'White Box Analysis' for more information.
Taking It Further
- Create a full blown Image Gallery application.
- Add Microsoft AJAX (formerly ATLAS) to display a source image on clicking the thumbnail.
- Add support for other files.
References
Conclusion
Well, as this article comes to an end, I hope that you have benefited and found some value with the Thumbnailer HTTP handler.
The world of opportunity with HTTP handlers and their counterpart HTTP modules is enormous and as broad as your imagination can take them. Creating logic outside the standard ASP.NET interfaces (*.aspx, *.ascx, *.asmx) can be very powerful and increase developer efficiency with respect to make-once-use-many object approach to software engineering.
Above all, I have learned to respect the hard work that goes into creating the universe of valuable online articles and references. If you haven't done so yet, I challenge you write an article on a topic that you've learned and that you feel would be of value to the user community, and publish it.
If you have any feedback on the quality of this article (or lack thereof), please let know. I'm always eager to learn from others.
Until next time, Go Noles…
Revision History
- Initial version, v1.0, 23 OCT 2006