Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Resource Server Handler Class For Custom Controls

0.00/5 (No votes)
4 Apr 2006 5  
A class implementing IHttpHandler for use in custom controls, to send embedded resources such as scripts, images, and style sheets to the client.

Introduction

When developing custom controls for ASP.NET, it may be necessary to create some client-side script that is used to interact with the custom control. There may also be image files that are used for certain elements of the control, such as buttons or style sheets that set the look of the control. A decision has to be made about how to deploy these resources with the custom control assembly.

The script can be built up using a StringBuilder or a static string, and inserted directly into the page using RegisterClientScriptBlock. This is okay for small scripts, but is unwieldy for much larger scripts, especially if they are complex enough to need debugging to work out problems during development. Scripts can also be embedded as resources in the assembly, retrieved at runtime, and again inserted into the page using RegisterClientScriptBlock. This is better than the StringBuilder approach for large scripts, but it is still inserted into the page and is rendered in its entirety, every time the page is requested. The more script code you have or the more controls on the page that render supporting script code, the larger the page gets. The script also cannot be cached on the client to save time on subsequent requests.

The scripts can be distributed as separate files along with the assembly. This solves the problem of the code being rendered in the page on each request and it can be cached on the client. However, it may complicate distribution of the custom control. It is no longer a simple XCOPY deployment as now scripts have to be installed along with the assembly. A number of factors such as whether it is a production or development server, whether or not the application is using SSL, and how the end-user's applications are set up, can affect where the scripts go, and you may end up with multiple copies in several locations. Versioning issues may also come into play if the scripts are modified in future releases of the control.

To solve these issues, I developed a class that implements the System.Web.IHttpHandler interface and acts as a resource server of sorts. The idea was inspired by examples I saw that showed how to render dynamic images using an ASPX page as the source of the image tag. The concept is basically the same for the resource server handler. You embed the resources in the control assembly and then add a simple class to your custom control assembly that implements the IHttpHandler interface. A section is added to the application's Web.Config file to direct resource requests to the handler class. The handler class uses parameters in the query string to determine the content type and sends back the appropriate resource such as a script, an image, a style sheet, or any other type of data that you need.

By having the resources embedded in the assembly and serving them as needed, there is as little code rendered in the page as possible by the controls. The resource handler responses can also be cached on the client, so performance can be improved as less information is sent to the client in subsequent page requests that utilize the same resources. This is most beneficial for users with slow dial-up connections, especially on forms that utilize controls with auto-postback enabled. The resources do not have to be deployed separately along with the assembly either, thus solving the problem of where to install the resources, as well as any issues involving versioning. We are back to a simple XCOPY deployment again.

The use of a resource server handler is not restricted to custom controls. It also adds the ability to do such things as request dynamic content, such as XML, using client-side script. For example, a request could be made to retrieve the results of a database query as XML using client-side script. The results could be used to populate a control or a popup window with information when it is needed rather than sending everything along with the page when first loaded. The following sections describe how to setup and utilize the resource server handler class.

A word about ASP.NET 2.0

The following will allow you to embed and serve resources in ASP.NET 1.1 applications as well as ASP.NET 2.0 applications. However, with ASP.NET 2.0 the ability to serve embedded web resources is a built-in feature and is simpler to implement. It makes use of embedded resources as described in here but utilizes attributes to define them along with a built-in Page.ClientScript method to render a link to them to the client. It does not require you to write a handler and does not require any entries in the Web.config file to define the handler or to allow anonymous access to them. Gary Dryden has already written a good article on how this works so I will just refer you to it rather than repeating what it has to say (WebResource ASP.NET 2.0 Explained[^]).

Add resources to your project

To keep things organized, store the resources in separate folders grouped by type (Scripts for script files, Images for image files, etc.). To create a new folder in the project, right click on the project name, select Add..., select New Folder, and enter the folder name. Add a new resource to the folder by right clicking on it, and selecting Add... and then Add New Item... to create a new item, or Add Existing Item... if you copied an existing file to the new folder. Once added to the project folder, right click on the file and select Properties. Change the Build Action property from Content to Embedded Resource. This step is most important as it indicates that you want the file to be embedded as a resource in the compiled assembly.

Add the ResSrvHandler class to your project

Add the ResSrvHandler.cs source file to your control's project and modify it as follows. TODO: comments have been added to help you find the sections that need modification.

Modify the namespace so that it matches the one for your custom control:

// TODO: Change the namespace to match your control's namespace.

namespace ResServerTest.Web.Controls
{

Modify the cResSrvHandlerPageName constant so that it matches the name you will use in the application's Web.Config file to direct resource requests to the class. I have chosen to use the custom control namespace with an .aspx extension. This keeps it unique and guarantees that it won't conflict with something in the end-user's application:

// TODO: Modify this constant to name the ASPX page that will be

// referenced in the application Web.Config file to invoke this

// handler class.


/// <summary>

/// The ASPX page name that will cause requests to get routed

/// to this handler.

/// </summary>

public const string cResSrvHandlerPageName =
              "ResServerTest.Web.Controls.aspx";

Modify the cImageResPath and cScriptResPath constants to point to your script and image paths. Add additional constants for other resource type paths as needed. The names of the embedded resources in the assembly are created by using the default namespace of the project plus the folder path to the resource. The default namespace is usually the same as the assembly name but you can modify it by right clicking on the project name, selecting Properties, and changing the Default Namespace option in the General section of the Common Properties entry. For the demo, the default namespace has been changed to match the namespace of the control, ResServerTest.Web.Controls, and the resource paths are Images and Scripts. As such, the constants are defined as shown below. The trailing "." should also be included. The resource name will be appended to the appropriate constant when loading it from the assembly.

Note that if you are using VB.NET, the default behavior of the compiler differs from the C# compiler. It will not append the default namespace to the front of the resource filename unless you explicitly include the command line option to tell it to do that. As such, for VB.NET projects, you can omit the path constants or set them to empty strings:

// TODO: Modify these two constants to match your control's

// namespace and the folder names of your resources. Add any

// additional constants as needed for other resource types.


/// <SUMMARY>

/// The path to the image resources

/// </SUMMARY>

private const string cImageResPath =
    "ResServerTest.Web.Controls.Images.";

/// <SUMMARY>

/// The path to the script resources

/// </SUMMARY>

private const string cScriptResPath =
    "ResServerTest.Web.Controls.Scripts.";

The ResourceUrl method can be called to format the URL used to retrieve an embedded resource from an assembly. The first version will retrieve the named resource from the assembly that contains the class. Simply pass it the name of the resource and it returns a URL that points to the resource.

The second version of the method can be used to extract embedded resources from assemblies other than the one containing the resource server class. Pass it the name of the assembly that contains the resource (without a path or extension, System.Web for example), the name of the resource handler that can retrieve it (i.e., the class defined in the cResSrvHandlerPageName constant), and the name of the resource to retrieve. When using this version, the name of the resource will be matched to the first resource that ends with the specified name. This allows you to skip the path name if you do not know it or extract resources from VB.NET assemblies which do not store the path:

/// <summary>

/// This can be called to format a URL to a resource name that is

/// embedded within the assembly.

/// </summary>

/// <param name="strResourceName">The name of the resource</param>

/// <param name="bCacheResource">Specify true to have the

/// resource cached on the client, false to never cache it.</param>

/// <returns>A string containing the URL to the resource</returns>

public static string ResourceUrl(string strResourceName,
                                         bool bCacheResource)
{
    return String.Format("{0}?Res={1}{2}", cResSrvHandlerPageName,
            strResourceName, (bCacheResource) ? "" : "&NoCache=1");
}

/// <summary>

/// This can be called to format a URL to a resource name that is

/// embedded within a different assembly.

/// </summary>

/// <param name="strAssemblyName">The name of the assembly that

/// contains the resource</param>

/// <param name="strResourceHandlerName">The name of the resource

/// handler that can retrieve it (i.e. the ASPX page name)</param>

/// <param name="strResourceName">The name of the resource</param>

/// <param name="bCacheResource">Specify true to have the

/// resource cached on the client, false to never cache it.</param>

/// <returns>A string containing the URL to the resource</returns>

public static string ResourceUrl(string strAssemblyName,
       string strResourceHandlerName, string strResourceName,
       bool bCacheResource)
{
    return String.Format("{0}?Assembly={1}&Res={2}{3}",
        strResourceHandlerName,
        HttpContext.Current.Server.UrlEncode(strAssemblyName),
        strResourceName, (bCacheResource) ? "" : "&NoCache=1");
}

The IHttpHandler.IsReusable property is implemented to indicate that the object instance can be reused for other requests. The IHttpHandler.ProcessRequest method is implemented to do all of the work. The first step is to determine the requested resource's name and its type. I use the filename's extension to determine the type. The code assumes that the query string parameter is called Res. Adjust this if you choose a different parameter name. Likewise, you can modify the code to determine the resource name and type in any number of ways depending on your needs:

/// <summary>

/// Load the resource specified in the query string and return

/// it as the HTTP response.

/// </summary>

/// <param name="context">The context object for the

/// request</param>

public void ProcessRequest(HttpContext context)
{
    Assembly asm;
    StreamReader sr = null;
    Stream s = null;

    string strResName, strType;
    byte[] byImage;
    int nLen;
    bool bUseInternalPath = true;

    // TODO: Be sure to adjust the QueryString names if you are

    // using something other than Res and NoCache.


    // Get the resource name and base the type on the extension

    strResName = context.Request.QueryString["Res"];
    strType = strResName.Substring(strResName.LastIndexOf(
                                            '.') + 1).ToLower();

The next step is to clear any current response and set up the caching options. If the NoCache query string parameter has not been specified, the class sets the necessary page caching options in the context.Response.Cache object. If it has been specified, the options are set such that the response will never be cached. The class defaults to having the response cached for one day. Adjust this as necessary for your controls. The response is set to vary caching by parameter name. The default class only has one parameter called Res. If you have additional parameters, be sure to add them as additional VaryByParams entries:

context.Response.Clear();

// If caching is not disabled, set the cache parameters so that

// the response is cached on the client for up to one day.

if(context.Request.QueryString["NoCache"] == null)
{
    // TODO: Adjust caching length as needed.


    context.Response.Cache.SetExpires(DateTime.Now.AddDays(1));
    context.Response.Cache.SetCacheability(HttpCacheability.Public);
    context.Response.Cache.SetValidUntilExpires(false);

    // Vary by parameter name. Note that if you have more

    // than one, add additional lines to specify them.

    context.Response.Cache.VaryByParams["Res"] = true;
}
else
{
    // The response is not cached

    context.Response.Cache.SetExpires(DateTime.Now.AddDays(-1));
    context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
}

The next section checks to see if the resource resides in another assembly. If the Assembly query string option has been omitted, it assumes the resource is in the same assembly as the class. If specified, it looks for the named assembly and, if found, searches for the resource within its manifest. When loading from a different assembly, the internal class path names are ignored and the name matched during the search is used instead:

// Get the resource from this assembly or another?

if(context.Request.QueryString["Assembly"] == null)
    asm = Assembly.GetExecutingAssembly();
else
{
    Assembly[] asmList =
        AppDomain.CurrentDomain.GetAssemblies();
    string strSearchName =
        context.Request.QueryString["Assembly"];

    foreach(Assembly a in asmList)
        if(a.GetName().Name == strSearchName)
        {
            asm = a;
            break;
        }

    if(asm == null)
        throw new ArgumentOutOfRangeException("Assembly",
                       strSearchName, "Assembly not found");

    // Now get the resources listed in the assembly manifest

    // and look for the filename. Note the fact that it is

    // matched on the filename and not necessarily the path

    // within the assembly. This may restricts you to using

    // a filename only once, but it also prevents the problem

    // that the VB.NET compiler has where it doesn't seem to

    // output folder names on resources.

    foreach(string strResource in asm.GetManifestResourceNames())
        if(strResource.EndsWith(strResName))
        {
            strResName = strResource;
            bUseInternalPath = false;
            break;
        }
}

As given, the class can serve up various image and script types, some styles for the demo, plus an additional XML file to demonstrate the NoCache option. A simple switch statement is used to determine what type to send back. The context.Response.ContentType property is set accordingly, the resource is retrieved, and it is then written to the response stream. You can expand or reduce the code to suit your needs:

switch(strType)
{
    case "gif":     // Image types

    case "jpg":
    case "jpeg":
    case "bmp":
    case "png":
    case "tif":
    case "tiff":
        if(strType == "jpg")
            strType = "jpeg";
        else
            if(strType == "png")
                strType = "x-png";
            else
                if(strType == "tif")
                    strType = "tiff";

        context.Response.ContentType = 
                                 "image/" + strType;

        if(bUseInternalPath == true)
            strResName = cImageResPath + strResName;

        s = asm.GetManifestResourceStream(strResName);

        nLen = Convert.ToInt32(s.Length);
        byImage = new Byte[nLen];
        s.Read(byImage, 0, nLen);

        context.Response.OutputStream.Write(
                                    byImage, 0, nLen);
        break;

    case "js":      // Script types

    case "vb":
    case "vbs":
        if(strType == "js")
            context.Response.ContentType = 
                                  "text/javascript";
        else
            context.Response.ContentType = 
                                    "text/vbscript";

        if(bUseInternalPath == true)
            strResName = cScriptResPath + strResName;

        sr = new StreamReader(
            asm.GetManifestResourceStream(strResName));
        context.Response.Write(sr.ReadToEnd());
        break;

    case "css":     // Some style sheet info

        // Not enough to embed so we'll write 

        // it out from here

        context.Response.ContentType = "text/css";

        if(bUseInternalPath == true)
            context.Response.Write(".Style1 { font-weight: bold; " +
                "color: #dc143c; font-style: italic; " +
                "text-decoration: underline; }\n" +
                ".Style2 { font-weight: bold; color: navy; " +
                "text-decoration: underline; }\n");
        else
        {
            // CSS from some other source

            sr = new StreamReader(
                asm.GetManifestResourceStream(strResName));
            context.Response.Write(sr.ReadToEnd());
        }
        break;

    case "htm":     // Maybe some html

    case "html":
        context.Response.ContentType = "text/html";

        sr = new StreamReader(
            asm.GetManifestResourceStream(strResName));
        context.Response.Write(sr.ReadToEnd());
        break;

    case "xml":     // Even some XML

        context.Response.ContentType = "text/xml";

        sr = new StreamReader(
            asm.GetManifestResourceStream(
            "ResServerTest.Web.Controls." + strResName));

        // This is used to demonstrate the NoCache option.

        // We'll modify the XML to show the current server

        // date and time.

        string strXML = sr.ReadToEnd();

        context.Response.Write(strXML.Replace("DATETIME",
            DateTime.Now.ToString()));
        break;

    default:    // Unknown resource type

        throw new Exception("Unknown resource type");
}

For simple text-based resources such as scripts, the StreamReader.ReadToEnd method can be used to retrieve the resource. For binary resources such as images, you must allocate an array and use StreamReader.Read to load the image into the array. Once loaded, you can write the array out to the client as shown above.

If an unknown resource type is requested or if it cannot be loaded from the assembly, an exception is thrown. For script resource types, the exception handler will convert the response to the appropriate type and send back a message box or alert so that the exception is displayed when the page loads. This will give you a chance to see what failed during development. For an XML resource, the exception handler will send back an XML response containing nodes with the resource name and the error description. For all other resource types, nothing is returned. Images will display a broken image placeholder, which serves as an indication that you may have done something wrong:

catch(Exception excp)
{
    XmlDocument xml;
    XmlNode node, element;

    string strMsg = excp.Message.Replace("\r\n", " ");

    context.Response.Clear();
    context.Response.Cache.SetExpires(
                    DateTime.Now.AddDays(-1));
    context.Response.Cache.SetCacheability(
                    HttpCacheability.NoCache);

    // For script, write out an alert describing the problem.

    // For XML, send an XML response containing the exception.

    // For all other resources, just let it display a broken

    // link or whatever.

    switch(strType)
    {
        case "js":
            context.Response.ContentType = "text/javascript";
            context.Response.Write(
                "alert(\"Could not load resource '" +
                strResName + "': " + strMsg + "\");");
            break;

        case "vb":
        case "vbs":
            context.Response.ContentType = "text/vbscript";
            context.Response.Write(
                "MsgBox \"Could not load resource '" +
                strResName + "': " + strMsg + "\"");
            break;

        case "xml":
            xml = new XmlDocument();
            node = xml.CreateElement("ResourceError");

            element = xml.CreateElement("Resource");
            element.InnerText = "Could not load resource: " +
                                                    strResName;
            node.AppendChild(element);

            element = xml.CreateElement("Exception");
            element.InnerText = strMsg;
            node.AppendChild(element);

            xml.AppendChild(node);
            context.Response.Write(xml.InnerXml);
            break;
    }
}
finally
{
    if(sr != null)
        sr.Close();

    if(s != null)
        s.Close();
}

Using the resource server handler in your control

Using the resource server handler in the custom control is very simple. Just add code to your class to render the attributes, script tags, or other resource types such as images that utilize the resource server page name. This is done by calling the ResSrvHandler.ResourceUrl method with the name of the resource and a Boolean flag indicating whether or not to cache it on the client. The demo control contains several examples.

// An image

img = new HtmlImage();

// Renders as:

// src="ResServerTest.Web.Controls.aspx?Res=FitHeight.bmp"

img.Src = ResSrvHandler.ResourceUrl("FitHeight.bmp", true);

// Call a function in the client-side script code registered below

img.Attributes["onclick"] = "javascript: FitToHeight()";

this.Controls.Add(img);

// Register the client-side script module

// Renders as: <script type='text/javascript'

// src='ResServerTest.Web.Controls.aspx?Res=DemoCustomControl.js'>

// </script>

this.Page.RegisterStartupScript("Demo_Startup",
    "<script type='text/javascript' src='" +
    ResSrvHandler.ResourceUrl("DemoCustomControl.js", true) +
    "'></script>");

// Register the style sheet

// Renders as: <link rel='stylesheet' type='text/css'

// href='ResServerTest.Web.Controls.aspx?Res=Styles.css'>

this.Page.RegisterScriptBlock("Demo_Styles",
    "<link rel='stylesheet' type='text/css' href='" +
    ResSrvHandler.ResourceUrl("Styles.css") + "'>\n");

As noted earlier, the lack of the NoCache query string option will cause the resources to be cached on the client. To turn off caching for a resource, simply specify false for the cache parameter of the ResourceUrl method, or add the NoCache parameter to the query string if hand-coding the URL. The demo ASPX page contains an example that retrieves an XML document from the control assembly. It uses the no caching option so that it displays the current time on the server every time the XML resource is requested. It also contains a couple of examples that retrieve resources from assemblies other than the custom control's assembly.

<script type='text/javascript'>
// Demonstrate the loading of uncached, 

// dynamic resources outside the

// control class. This gets some XML 

// from the resource server page.

function funShowXML()
{
  window.open(
    'ResServerTest.Web.Controls.aspx?Res=Demo.xml&NoCache=1', 
    null,
    'menubar=no,personalbar=no,resizable=yes,' + 
    'scrollbars=yes,status=no,' +
    'toolbar=no,screenX=50,screenY=50,' + 
    'height=400,width=800').focus()
}
</script>

Using the control and the resource server handler in an application

In the application's project, add a reference to your custom control's assembly and add your custom control to the application's forms in the normal fashion. To use the resource server handler, add an entry in the <system.web> section of your application's Web.Config file like the following:

<!-- Demo Control Resource Server Handler
     Add this section to map the resource requests to the resource
     handler class in the custom control assembly. -->
<httpHandlers>
  <add verb="*" path="ResServerTest.Web.Controls.aspx"
         type="ResServerTest.Web.Controls.ResSrvHandler,
         ResServerTest.Web.Controls"/>
</httpHandlers>

Modify the path attribute so that it matches the ResSrvHandler.cResSrvHandlerPageName constant. Modify the type attribute to reference your resource server handler's class name (including its namespace) followed by a comma and then the name of the assembly. This entry causes any requests containing the page name specified in the path attribute, regardless of the folder, to get mapped to your resource handler class.

Allowing anonymous access to resources when using forms-based authentication

When using forms-based authentication to secure an entire application, access to the resources is prevented on the logon page because the above HTTP handler uses an ASPX page name to route the requests for resources to the handler. As such, it is treated like any other request for a normal page, and instead of returning the resource, ASP.NET redirects the request to the login page as well. To prevent this and allow anonymous access to the resource server handler from the logon page, the following section should be added to the <configuration> section of your Web.config file:

<!-- This is needed to allow anonymous access to the resource server
     handler for the ResServerTest.Web.Controls namespace from a logon
     web form when using forms-based authentication. -->
<location path="ResServerTest.Web.Controls.aspx">
    <system.web>
        <authorization>
            <allow users="*"/>
        </authorization>
    </system.web>
</location>

This allows all users to access the resources without authentication, thus allowing the classes and controls in the assembly that utilize the resource server handler to be used on the logon web form. Just modify the page name in the path attribute of the location tag to match the one used in the HTTP handler section.

Common errors and problems

The most common errors when using the resource server handler are misspelling the resource name and forgetting to change the Build Action property to Embedded Resource. In both cases, the error "Value cannot be null. Parameter name: stream" is returned for script and XML resources. Image resources just won't display anything. The demo project contains examples of these errors.

Another common error is misspelling the ASPX page name when referencing it in the Web.Config file or in Web Forms when retrieving the dynamic content. In these cases, you will get broken links or "resource not found" errors. If you misspell the class or assembly name in the Web.Config file, the application will fail to start, and you will get an error telling you that it cannot find the specified type or assembly. The type or assembly shown in the error message will be the incorrect name for the resource handler class or its assembly. Correcting the names will resolve the errors.

A problem that can occur during development is making modifications to the resources and then not seeing those changes reflected when testing the control. The reason for this is that embedded resources do not create a build dependency. As such, modifying them does not cause a rebuild of the assembly. When making such changes, you just have to remember to always force a rebuild of the assembly so that it embeds the updated resources. You may also have to force a refresh of the page to get it to download the updated resource (Ctrl+F5 in Internet Explorer for example).

You can test the retrieval of a resource and view what gets returned by opening the browser and entering the URL to the resource. For text-based resources, prefix the URL with "view-source:". For example:

view-source:http://localhost/ResSrvTest/ResServerTest.Web.
Controls.aspx?Res=DemoCustomControl.js

This would retrieve the DemoCustomControl.js file and display it in a Notepad window.

The demo

To try out the demo application and custom control, create a virtual directory in IIS called ResSrvTest and point it at the DotNet\Web\ReServerTest\ResSrvTest folder. The startup page is WebForm1.aspx. The demo project is set up to compile and run on a development machine that has Visual Studio .NET 2003 and IIS running on it. If you are using a remote server, you will need to set up the virtual directory, build the demo custom control separately, and copy it and the demo application files to the server location.

Conclusion

I have used this method in my custom controls that utilize client-side script and image files, and have found it to be quite useful. Development of the controls involving client-side script has been made easier as has the deployment of the control assemblies.

Revision history

  • 04/02/2006
    • Removed ReadBinaryResource as it is not needed. Also added notes about the new support for web resources in ASP.NET 2.0.
  • 06/27/2004

    Made some code adjustments. Made the following additions based on suggestions from Adam Pawsey:

    • Added some static helper methods to create resource links.
    • Added support for loading resources from other assemblies besides the one containing the resource handler class.
  • 07/19/2003
    • Added a section on getting the resource server to work from a logon web form in an application using forms-based authentication. Updated the XML code documentation. Rebuilt the project with Visual Studio .NET 2003.
  • 04/09/2003
    • Changed the resource server class so that it implements System.Web.IHttpHandler instead of deriving from System.Web.UI.Page. This improves performance and further simplifies usage of the class.
  • 04/06/2003
    • Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here