Graphical Controls

By writing to the output stream with HtmlTextWriter, a custom control can render anything that can be expressed in HTML. Controls that require more latitude than HTML provides can return graphical images. The images can be stored statically (for example, in JPEG files) on the server, or they can be generated at run time and even customized for each request. Returning dynamically generated images frees controls from the limitations of HTML and opens the door to a world of possibilities, from controls that render graphs and pie charts to controls that render maps, formatted reports, and virtually anything else you can dream up.

The secret to authoring a graphical control is to code its Render method to return an <img> tag. If the image is static—that is, if it’s stored in a file on the server—the tag’s Src attribute identifies the image file:

<img?src="staticimage.jpg">

If the image is dynamically generated, however, the Src attribute must point to a URL that creates the image on the fly. The following <img> tag references a URL that dynamically generates an image based on input passed in the query string:

<img?src="imagegen.ashx?shape=circle&color=red">

What is ImageGen.ashx? It’s not a file; it’s an HTTP handler. Specifically, it’s an HTTP handler you write that parses a query string and returns a dynamically generated image. We haven’t talked about HTTP handlers yet. Let’s remedy that shortcoming right now.

HTTP Handlers

HTTP handlers are one of the fundamental building blocks of ASP.NET. An HTTP handler is a class that handles HTTP requests for a specific endpoint (URL) or set of endpoints on a Web server. HTTP handlers built into ASP.NET handle requests for ASPX files, ASCX files, and other ASP.NET file types. In addition, you can extend ASP.NET with HTTP handlers of your own. Entries in Web.config map URLs to HTTP handlers. (We haven’t talked about Web.config and the role that it plays in configuring ASP.NET applications yet, but don’t worry: you’ll learn all about it in the next chapter.) The following statements in a Web.config file map requests for ImageGen.ashx targeted at this directory (the directory that hosts Web.config) and its subdirectories to a class named ImageGen in an assembly named DynaImageLib.dll:

<httpHandlers>
??<add?verb="*" path="ImageGen.ashx" type="ImageGen,?DynaImageLib" />
</httpHandlers>

When an HTTP request arrives for ImageGen.ashx, ASP.NET instantiates ImageGen and passes it the request. Assuming ImageGen is an image generator, it responds by creating an image and returning it in the HTTP response. Here’s a generic template for an HTTP handler that creates an image in memory and returns it to the requestor as a JPEG. The hard part—building the image and writing it out to the HTTP response as a JPEG-formatted bit stream—is vastly simplified by the FCL’s Bitmap and Graphics classes:

public?class?ImageGen?:?IHttpHandler
{
????public?void?ProcessRequest?(HttpContext?context)
????{
????????//?Create?a?bitmap?that?measures?128?pixels?square
????????Bitmap?bitmap?=?new?Bitmap?(128,?128,
????????????PixelFormat.Format32bppArgb);

????????//?Create?a?Graphics?object?for?drawing?to?the?bitmap
????????Graphics?g?=?Graphics.FromImage?(bitmap);

????????//?TODO:?Use?Graphics?methods?to?draw?the?image
??????????.
??????????.
??????????.
????????//?Set?the?response's?content?type?to?image/jpeg
????????context.Response.ContentType?= "image/jpeg";

????????//?Write?the?image?to?the?HTTP?response
????????bitmap.Save?(context.Response.OutputStream,?ImageFormat.Jpeg);

????????//?Clean?up?before?returning
????????bitmap.Dispose?();
????????g.Dispose?();
????}

????public?bool?IsReusable
????{
????????//?Returning?true?enables?instances?of?this?class?to?be
????????//?pooled?and?reused.?Return?false?if?ImageGen?instances
????????//?should?NOT?be?reused.
????????get?{?return?true;?}
????}
}?

ImageGen can be deployed in its own assembly or in the same assembly as a control. Once deployed, it’s invoked by ASP.NET whenever an HTTP request arrives for ImageGen.ashx. “Invoke” means ASP.NET instantiates ImageGen and calls its ProcessRequest method. ProcessRequest receives an HttpContext object whose Request property provides access to input parameters encoded in the query string. ProcessRequest writes to the HTTP response using the HttpContext object’s Response property. To return an image, ProcessRequest saves the bits making up the image to the stream represented by HttpContext.Response.OutputStream.

Incidentally, it doesn’t matter that there is no file named ImageGen.ashx; what’s important is that ImageGen.ashx is mapped to ImageGen via Web.config. You don’t have to use the file name extension ASHX for HTTP handlers, but ASHX is a widely used convention. Using ASHX as the handler’s file name extension also prevents you from having to register a special file name extension in the IIS metabase. (If you picked an arbitrary file name extension—say, IGEN—for your HTTP handler, you’d also have to map *.igen to Aspnet_isapi.dll in the IIS metabase. Otherwise, IIS wouldn’t forward requests for IGEN files to ASP.NET.) ASHX files are already mapped to ASP.NET in the IIS metabase, so requests for files with ASHX file name extensions are automatically handed off to ASP.NET. You can also write ASPX files that generate and return images, but using a dedicated HTTP handler is cleaner because it requires no additional files on the server.

With this background in mind, let’s close the chapter with a control that returns a dynamically generated image depicting an odometer. One application for such a control is to implement the ubiquitous hit counters found on sites all over the Web.

The Odometer Control

The Odometer control shown in Figure 8-29 renders itself using dynamically generated images. The control’s programmatic interface consists of the following public properties:

Property

Description

Count

Gets and sets the value displayed by the control

Digits

Gets and sets the number of digits displayed

Width

Gets and sets the control’s width

Height

Gets and sets the control’s height

ForeColor

Gets and sets the color of the control’s numbers

BackColor1

Gets and sets the first of two background colors behind the numbers

BackColor2

Gets and sets the second of two background colors behind the numbers

BorderColor

Gets and sets the color of the control’s border

The two BackColor properties merit further explanation. The Odometer control uses a pair of LinearGradientBrushes (discussed in Chapter 4) to paint the background behind the numbers. It exposes the colors of these brushes through BackColor1 and BackColor2. By default, BackColor1 is black and BackColor2 is light gray, which produces a background that fades from black to light gray and then back to black again, yielding the realistic look depicted in Figure 8-29. If you prefer a flat background, set BackColor1 and BackColor2 to the same color. Setting both to red produces red cells behind the numbers.

Figure 8-29
The Odometer control in action.

It’s simple to add an Odometer control to a Web page and configure it to display the number 1,000:

<%@?Register?TagPrefix="win" Namespace="Wintellect"
??Assembly="OdometerControl" %>
????.
????.
????.
<win:Odometer?Count="1000" RunAt="server" />

The following statement configures the control to display five digits (“01000”) instead of the four that would normally be displayed for 1,000:

<win:Odometer?Count="1000" Digits="5" RunAt="server" />

The next statement does the same thing, but it also configures the control to display numbers against a flat black background:

<win:Odometer?Count="1000" Digits="5" RunAt="server"
??BackColor1="black" BackColor2="black" />

If you’d like, you can set Count’s value at run time by initializing it from a Page_Load handler:

<win:Odometer?ID="MyOdometer" RunAt="server" />
????.
????.
????.
<script?language="C#" runat="server">
??void?Page_Load?(Object?sender,?EventArgs?e)
??{
??????MyOdometer.Count?=?1000;
??}
</script>

In all likelihood, you’d retrieve the count from a database or other data source rather than hardcode into the ASPX file.

Before using the control in a Web page, you must do the following:

Odometer outputs <img> tags whose Src attributes reference OdometerImageGen.ashx. Web.config maps requests for OdometerImageGen.ashx to the HTTP handler OdometerImageGen, which lives in the same DLL as the control.

Web.config
<configuration>
??<system.web>
????<httpHandlers>
??????<add?verb="*" path="OdometerImageGen.ashx"
????????type="Wintellect.OdometerImageGen,?OdometerControl" />
????</httpHandlers>
??</system.web>
</configuration>
Figure 8-30
Web.config file for the Odometer control.
How the Odometer Control Works

Odometer.cs (Figure 8-31) is a fine example of how to write custom controls that output dynamically generated images. The Odometer class represents the control itself. Its Render method outputs an <img> tag that points to OdometerImageGen.ashx as the image source. The URL includes a query string that provides the handler with all the information it needs regarding the odometer’s appearance:

<img?src="OdometerImageGen.ashx?Count=1000&Digits=5...">

When the browser fetches OdometerImageGen.ashx from the server, ASP.NET activates OdometerImageGen thanks to the following statement in Web.config:

<add?verb="*" path="OdometerImageGen.ashx"
??type="Wintellect.OdometerImageGen,?OdometerControl" />

OdometerImageGen is implemented in Odometer.cs alongside Odometer. Its ProcessRequest method generates an image based on the inputs contained in the query string. ProcessRequest offloads the actual work of creating the image to a local method named GenerateImage. It then transmits the image back in the HTTP response by calling Bitmap.Save on the Bitmap returned by GenerateImage and directing the output to the Response object’s OutputStream.

Odometer.cs
using?System;
using?System.Web;
using?System.Web.UI;
using?System.Drawing;
using?System.Drawing.Drawing2D;
using?System.Drawing.Imaging;
using?System.Text;

namespace?Wintellect
{
????public?class?Odometer?:?Control
????{
????????int?MyCount?=?0;
????????int?MyDigits?=?0;
????????int?MyWidth?=?128;
????????int?MyHeight?=?48;
????????Color?MyForeColor?=?Color.White;
????????Color?MyBackColor1?=?Color.Black;
????????Color?MyBackColor2?=?Color.LightGray;
????????Color?MyBorderColor?=?Color.Gray;

????????public?int?Count
????????{
????????????get?{?return?MyCount;?}
????????????set
????????????{
????????????????if?(value?>=?0)
????????????????????MyCount?=?value;
????????????????else
????????????????????throw?new?ArgumentOutOfRangeException?();
????????????}
????????}

????????public?int?Digits
????????{
????????????get?{?return?MyDigits;?}
????????????set
????????????{
????????????????if?(value?>=?0)
????????????????????MyDigits?=?value;
????????????????else
????????????????????throw?new?ArgumentOutOfRangeException?();
????????????}
????????}

????????public?int?Width
????????{
????????????get?{?return?MyWidth;?}
????????????set
????????????{
????????????????if?(value?>=?0)
????????????????????MyWidth?=?value;
????????????????else
????????????????????throw?new?ArgumentOutOfRangeException?();
????????????}
????????}

????????public?int?Height
????????{
????????????get?{?return?MyHeight;?}
????????????set
????????????{
????????????????if?(value?>=?0)
????????????????????MyHeight?=?value;
????????????????else
????????????????????throw?new?ArgumentOutOfRangeException?();
????????????}
????????}

????????public?Color?ForeColor
????????{
????????????get?{?return?MyForeColor;?}
????????????set?{?MyForeColor?=?value;?}
????????}

????????public?Color?BackColor1
????????{
????????????get?{?return?MyBackColor1;?}
????????????set?{?MyBackColor1?=?value;?}
????????}

????????public?Color?BackColor2
????????{
????????????get?{?return?MyBackColor2;?}
????????????set?{?MyBackColor2?=?value;?}
????????}

????????public?Color?BorderColor
????????{
????????????get?{?return?MyBorderColor;?}
????????????set?{?MyBorderColor?=?value;?}
????????}

????????protected?override?void?Render?(HtmlTextWriter?writer)
????????{
????????????StringBuilder?builder?=?new?StringBuilder?();

????????????builder.Append?("OdometerImageGen.ashx?");
????????????builder.Append?("Count=");
????????????builder.Append?(Count);
????????????builder.Append?("&Digits=");
????????????builder.Append?(Digits);
????????????builder.Append?("&Width=");
????????????builder.Append?(Width);
????????????builder.Append?("&Height=");
????????????builder.Append?(Height);
????????????builder.Append?("&ForeColor=");
????????????builder.Append?(ForeColor.ToArgb?().ToString?());
????????????builder.Append?("&BackColor1=");
????????????builder.Append?(BackColor1.ToArgb?().ToString?());
????????????builder.Append?("&BackColor2=");
????????????builder.Append?(BackColor2.ToArgb?().ToString?());
????????????builder.Append?("&BorderColor=");
????????????builder.Append?(BorderColor.ToArgb?().ToString?());

????????????writer.WriteBeginTag?("img");
????????????writer.WriteAttribute?("src",?builder.ToString?());
????????????if?(ID?!=?null)
????????????????writer.WriteAttribute?("id",?ClientID);
????????????writer.Write?(HtmlTextWriter.TagRightChar);
????????}
????}

????public?class?OdometerImageGen?:?IHttpHandler
????{
????????public?void?ProcessRequest?(HttpContext?context)
????????{
????????????//?Extract?input?values?from?the?query?string
????????????int?Count?=?Convert.ToInt32?(context.Request["Count"]);
????????????int?Digits?=?Convert.ToInt32?(context.Request["Digits"]);
????????????int?Width?=?Convert.ToInt32?(context.Request["Width"]);
????????????int?Height?=?Convert.ToInt32?(context.Request["Height"]);

????????????Color?ForeColor?=?Color.FromArgb
????????????????(Convert.ToInt32?(context.Request["ForeColor"]));
????????????Color?BackColor1?=?Color.FromArgb
????????????????(Convert.ToInt32?(context.Request["BackColor1"]));
????????????Color?BackColor2?=?Color.FromArgb
????????????????(Convert.ToInt32?(context.Request["BackColor2"]));
????????????Color?BorderColor?=?Color.FromArgb
????????????????(Convert.ToInt32?(context.Request["BorderColor"]));

????????????//?Generate?an?image?to?return?to?the?client
????????????Bitmap?bitmap?=?GenerateImage?(Count,?Digits,
????????????????Width,?Height,?ForeColor,?BackColor1,?BackColor2,
????????????????BorderColor);

????????????//?Set?the?content?type?to?image/jpeg
????????????context.Response.ContentType?= "image/jpeg";

????????????//?Write?the?image?to?the?HTTP?response
????????????bitmap.Save?(context.Response.OutputStream,
????????????????ImageFormat.Jpeg);

????????????//?Clean?up
????????????bitmap.Dispose?();
????????}

????????public?bool?IsReusable
????????{
????????????get?{?return?true;?}
????????}

????????Bitmap?GenerateImage?(int?Count,?int?Digits,
????????????int?Width,?int?Height,?Color?ForeColor,?Color?BackColor1,
????????????Color?BackColor2,?Color?BorderColor)
????????{
????????????const?int?BorderWidth?=?4;
????????????const?int?MinCellWidth?=?16;
????????????const?int?MinCellHeight?=?24;

????????????//?Make?sure?Digits?is?sufficient?for?Count?to?be?displayed
????????????int?digits?=?Digits;
????????????int?places?=?Places?(Count);
????????????if?(digits?<?places)
????????????????digits?=?places;

????????????//?Compute?the?width?of?a?single?character?cell?and
????????????//?the?width?and?height?of?the?entire?image
????????????int?CellWidth?=?System.Math.Max?(Width?/?digits,
????????????????MinCellWidth);
????????????Width?=?(CellWidth?*?digits)?+?BorderWidth;
????????????Height?=?System.Math.Max?(Height,?MinCellHeight);

????????????//?Create?an?in-memory?bitmap
????????????Bitmap?bitmap?=?new?Bitmap?(Width,?Height,
????????????????PixelFormat.Format32bppArgb);

????????????//?Create?the?fonts?and?brushes?that?will?be?used?to
????????????//?generate?the?image
????????????Font?font?=?new?Font?("Arial",?Height?/?2);
????????????Brush?brushForeColor?=?new?SolidBrush?(ForeColor);
????????????Brush?brushBorderColor?=?new?SolidBrush?(BorderColor);

????????????//?Create?a?Graphics?object?that?can?be?used?to?draw?to
????????????//?the?bitmap
????????????Graphics?g?=?Graphics.FromImage?(bitmap);

????????????//?Fill?the?bitmap?with?the?border?color
????????????g.FillRectangle?(brushBorderColor,?0,?0,?Width,?Height);

????????????//?Create?a?StringFormat?object?for?displaying?text
????????????//?that?is?centered?horizontally?and?vertically
????????????StringFormat?format?=?new?StringFormat?();
????????????format.Alignment?=?StringAlignment.Center;
????????????format.LineAlignment?=?StringAlignment.Center;

????????????//?Initialize?the?values?used?to?extract?individual
????????????//?digits?from?Count
????????????int?div1?=?(int)?System.Math.Pow?(10,?digits);
????????????int?div2?=?div1?/?10;

????????????//?Draw?the?digits?and?their?backgrounds
????????????for?(int?i=0;?i<digits;?i++)?{
????????????????Rectangle?rect?=
????????????????????new?Rectangle?(i?*?CellWidth?+?BorderWidth,
????????????????????BorderWidth,?CellWidth?-?BorderWidth,
????????????????????Height?-?(2?*?BorderWidth));

????????????????Rectangle?top?=?rect;
????????????????top.Height?=?(rect.Height?/?2)?+?1;
????????????????Rectangle?bottom?=?rect;
????????????????bottom.Y?+=?rect.Height?/?2;
????????????????bottom.Height?=?rect.Height?/?2;

????????????????Brush?brushBackColor1?=
????????????????????new?LinearGradientBrush?(top,?BackColor1,
????????????????????BackColor2,?LinearGradientMode.Vertical);

????????????????Brush?brushBackColor2?=
????????????????????new?LinearGradientBrush?(bottom,?BackColor2,
????????????????????BackColor1,?LinearGradientMode.Vertical);

????????????????g.FillRectangle?(brushBackColor2,?bottom);
????????????????g.FillRectangle?(brushBackColor1,?top);

????????????????string?num?=?((Count?%?div1)?/?div2).ToString?();
????????????????g.DrawString?(num,?font,?brushForeColor,?rect,?format);

????????????????div1?/=?10;
????????????????div2?/=?10;

????????????????brushBackColor1.Dispose?();
????????????????brushBackColor2.Dispose?();
????????????}

????????????//?Clean?up?and?return
????????????font.Dispose?();
????????????brushForeColor.Dispose?();
????????????brushBorderColor.Dispose?();
????????????g.Dispose?();

????????????return?bitmap;
????????}

????????//?Compute?the?number?of?places?(digits)?in?an?input?value
????????int?Places?(int?val)
????????{
????????????int?count?=?1;
????????????while?(val?/?10?>?0)?{
????????????????val?/=?10;
????????????????count++;
????????????}
????????????return?count;
????????}
????}
}
Figure 8-31
Odometer control.