CoreCoder
LANGUAGES: C#
ASP.NET VERSIONS: 1.x | 2.0
Package Up Client Files
Embed Control Resources in the Assembly and Keep them Hidden
By Dino Esposito
Resources are an old acquaintance for many seasoned Windows programmers. Most pre-.NET Windows applications were used to embed dialogs boxes, images, icons, localizable text, and even custom data in a distinct segment within the executable file. Keeping resources distinct from the rest of the code has several significant benefits. In particular, you can localize, or simply replace, resources without even recompiling the source code. In the realm of Web applications, however, basic conditions lacked for code and resources separation to be a design and maintenance edge. The advent of ASP.NET finally started the process of Web componentization a process that is going to receive a definite boost with the introduction of ASP.NET 2.0.
In classic ASP, the structure of a Web application was such a tangled miscellany of elements (markup, literals, code blocks, COM objects, script) it was difficult for anybody to distinguish business components from UI blocks and both from glue code. In ASP.NET, controls provide an efficient way to pack UI blocks and code together and expose the resulting aggregate through a set of well-known interfaces and a public object model. Regardless of the supported programming model, an ASP.NET control always renders up as HTML and can make use of special resources, such as script code and images. The output of a control is then merged with the output of the hosting page and generates a unique markup string that embeds images and script code for all controls and the page itself.
How should you design and implement an ASP.NET control that needs to incorporate images and script? And what s going to change with the forthcoming hot new version of ASP.NET?
Find Your Resources
Let s focus on a concrete scenario and imagine you ve been assigned the task of creating a custom panel control that mimics the user interface of Windows XP panels (see Figure 1). The control, named GraphicPanel, is a relatively simple wrapper that works on the rendering engine of the Panel class. The new control overrides two methods, RenderBeginTag and RenderEndTag, to build an outer table around the default output of the panel control. The resulting table counts two rows: the header and the body. The body coincides with the output of the base class; the top-most row renders the header you see in Figure 1. The top-most row consists of three cells: left edge, right edge, and title. The side cells display an image each. How do you specify the URLs of these images?
Figure 1: A graphic panel control.
Figure 2 shows a small fragment of the control s source code, where a couple of string properties clearly stand out. They are LeftEdgePictureUrl and RightEdgePictureUrl. As the names suggests, both contain the URL to the image to be used on the left and right edge of the header row. The two properties prefigure a usage like the following example. After dropping the GraphicPanel control on a Web form, the page developer selects it in the designer and edits the contents of the properties in the Visual Studio.NET property grid. In the end, the two images are definitely part of the control s user interface, but are not embedded with the control s assembly. They remain external resources belonging to the application rather than to the control. Among other things, this means that you need to bundle the images along with the control s assembly, and you must remember to move images from site to site if you use the same control across different applications. Even more importantly, if at a certain time you reorganize the folders under your root, you need to tweak all the pages that use the control to make sure it correctly points to the images.
public class GraphicPanel : Panel
{
public string LeftEdgePictureUrl { ... }
public string RightEdgePictureUrl { ... }
public string HeaderText { ... }
public override void RenderBeginTag(HtmlTextWriter writer)
{ ... }
public override void RenderEndTag(HtmlTextWriter writer)
{ ... }
}
Figure 2: Skeleton of the GraphicPanel class.
Although limiting, this approach is easy to code and can be used with both ASP.NET 1.x and 2.0. But there has to be a better approach.
Find a Proxy for Your Resources
So you want to build a control that uses images as part of its own user interface, and you especially want to be able to pack images and code within the same assembly, thus zeroing deployment and maintenance costs. Note that the problem here is not packing images and code together that s exactly what resources are for, isn t it? The point is how to retrieve the image out of the assembly and serve it to the user through an HTML tag. You need a URL to associate with the Src property of the tag. Once embedded in an assembly, though, the image can t be referenced as a URL that the browser can translate into a remote path.
You need an additional component that can be expressed as a URL and retrieves and downloads the bytes of the image to the client. Here s an abstract example:
graphicPanel1.LeftEdgePictureUrl = MyUrl + "?id=leftgrip";
What s Behind the MyUrl Placeholder?
You can start by writing a custom HTTP handler in the same assembly that contains the control. Next, you bind the handler to a fixed URL in the web.config file. The ASP.NET HTTP runtime guarantees that it ll be invoked whenever a request comes in for that URL. Here s some sample code to add to the application s web.config file:
type="YourCompany.ImageService" /> Both the handler s path name and type used in the
preceding code snippet are arbitrary. In particular, you should note that
imageservice.axd doesn t have to be a physical resource located on the Web
server. It is a mere name that the ASP.NET runtime maps to a particular method
on the specified type. In the end, what really matters is that the current
assembly that is, the one you re building that contains the custom control is
plumbed as shown in Figure 3. namespace Expoware { public class ImageService
: IHttpHandler { public bool IsReusable
{ get { return true;
} } public void
ProcessRequest(HttpContext context) { string imageID =
context.Request["id"]; if (imageID == null) return; Image img =
LoadImageFromResources(imageID); if (img != null) { WriteImage(context, img); img.Dispose(); } } private void
WriteImage(HttpContext context, System.Drawing.Image
img) { context.Response.ContentType = "image/gif"; MemoryStream ms =
new MemoryStream(); img.Save(ms,
ImageFormat.Gif); ms.WriteTo(context.Response.OutputStream); ms.Close(); } private Image
LoadImageFromResources(string imageID) { Assembly dll =
Assembly.GetExecutingAssembly(); Bitmap img = new
Bitmap (dll.GetManifestResourceStream(imageID)); return img; } } } Figure 3: A sample
HTTP handler for serving images. The class is an HTTP handler and implements IHttpHandler.
Any request that hits imageservice.axd is served invoking the ProcessRequest
method on the handler class. In the code sample illustrated in Figure 3, the
ProcessRequest method uses the content of the ID query string parameter (the
name ID is arbitrary) to select an image from the embedded assembly s
resources. The bytes of the image are used to build a System.Drawing.Bitmap
object, which is then serialized to the client: img.Save(ms, ImageFormat.Gif); It is interesting to notice that the System.Drawing.Bitmap
object is format-agnostic; the preceding line takes the image and downloads it
to the client as a GIF file. In other words, you can embed JPEG images and
serve them to the client as GIFs. The picture properties of the GraphicControl
class are set as follows: graphicPanel1.LeftEdgePictureUrl = "imageservice.axd?id=Expoware.LeftGrip.gif"; graphicPanel1.RightEdgePictureUrl = "imageservice.axd?id=Expoware.RightGrip.gif"; How do you add images to an assembly? You simply select
Add | Existing Item from the project s context menu and pick up any image you
need. Next, select the image in the project and make sure it is configured as
an Embedded Resource in the Properties window. Finally, you ought to rename the
file according to a particular convention. The file name must be prefixed with
the control s namespace. For example, if the control is hosted in the Expoware
namespace, the file name must be Expoware.[FileName].[Extension]. imageservice.axd acts as a proxy for all images that a
control needs to display. It is a component that uses your own code to retrieve
and serve images dynamically. In this case, images are packed with the control
and don t have to be deployed or maintained separately. The LeftEdgePictureUrl
property can now be written as follows: public string LeftEdgePictureUrl { get { object o =
ViewState["LeftEdgePicture"]; if (o == null) return
"imageservice.axd?ID=Expoware.LeftGrip.gif"; return (string)o; } set {
ViewState["LeftEdgePicture"] = value; } } If you want to use a custom picture, you simply set the
property to any URL that corresponds to a physical resource. Otherwise, if you
take the default value, the control picks out the images from the same
assembly. The GraphicPanel control now can appropriately be defined as a
self-contained control. The big picture doesn t change much in ASP.NET 2.0, meaning
that you still need to use an HTTP handler to retrieve the bytes from the
executing assembly. However, the great news is that the tools to do that are
now available with the system. They are the WebResource.axd handler and the
WebResource attribute. The former is a system component that controls use to
retrieve resources from assemblies: script, strings, style sheets, images,
whatever. The latter is an assembly-level metadata attribute that allows you to
mark embedded resources as URL-accessible. In the AssemblyInfo file of your
project, you add code like this: [assembly: WebResource("Expoware.LeftGrip.gif", "image/gif")] To programmatically access the resource, you use a new
method on the Page class the GetWebResourceUrl method: public string LeftEdgePictureUrl { get { object o = ViewState["LeftEdgePicture"]; if (o == null) return
Page.GetWebResourceUrl(typeof(GraphicPanel), "Expoware.LeftGrip.gif"); return (string)o; } set {
ViewState["LeftEdgePicture"] = value; } } The method looks in the assembly that contains the
specified type for a marked Web resource with the specified name. The resource
is then served to the browser and cached locally. With this tool, ASP.NET 2.0
enables control developers to package client files without having to scatter them
into the file system, and without enabling external handlers. It is definitely
a cleaner and simpler form of deployment. Rich controls sometimes need embedded resources to render
out their own user interface. Resources may include JavaScript files, GIF or
JPEG images, style sheets, and anything you need outside code to serve up a
control. When building a control, the simplest approach consists of referencing
any external resource through a URL. The code has no overhead at all, but the
approach jeopardizes the overall manageability of the project, and sparse files
proliferate in the file system and may be even left around. A second approach entails creating a custom HTTP handler
and binding it to a public name. You reference external resources (images, specifically)
through this sort of proxy. The handler wraps the code to load the image from
the assembly, but requires you to edit the web.config file to register the
public name with the application. These two solutions work great with ASP.NET
1.x and ASP.NET 2.0. In ASP.NET 2.0, however, a new set of specifically
designed tools the WebResource attribute and WebResource.axd handler makes
working with resources as seamless as possible, and removes all the drawbacks
we ve considered so far. The
sample code accompanying this article is available for download. Dino Esposito is a
Wintellect trainer and consultant who specializes in ASP.NET and ADO.NET.
Author of Programming Microsoft ASP.NET
and Introducing ASP.NET 2.0, both from
Microsoft Press, Dino also helped several companies architect and build
effective products for ASP.NET developers. Dino is the cofounder of http://www.VB2TheMax.com, a popular portal
for VB and .NET programmers. Write to him at mailto:[email protected] or join
the blog at http://weblogs.asp.net/despos. What s New in ASP.NET 2.0
Fast Facts