Rory Primrose

Learn from my mistakes, you don't have time to make them yourself

View project on GitHub

Creating Web Custom Controls With ASP.Net 1.1 - Part VI - Handling Dependencies

The main purpose of creating controls is so they can be reused in other projects. The beauty of Web Custom Controls as opposed to Web User Controls is that they are compiled into an assembly which can easily be copied between ASP.Net projects.

When I am creating controls, I always have dependencies to support the them. At the very least, I have JavaScript that needs to be rendered for client-side behaviours of the control. I have written several controls that also rely on style-sheets and images to be used with the control. Deployment of these dependencies becomes more than a little nightmare.

Take an image for example. What happens when a control needs to render an image to the browser? It needs to render some HTML like <IMG src= ‘test.gif’ />. The browser then makes a request to the web server for test.gif. Thats all very well and good, but to deploy this control, you not only need to copy the controls assembly to the deployment bin folder, but also need to deploy test.gif to the correct folder to be requested by the browser. Each dependency of the control needs to be copied across each of the ASP.Net projects that use the control. This very quickly becomes a maintenance problem.

What we want to do is package all of these dependencies into the controls assembly. To achieve this, move the required files from the ASP.Net project and put them in the controls project. For each of these files, go to the property grid and set their Build Action to Embedded Resource. This will add each file as a resource to the controls assembly when it is compiled. We have now solved the deployment problem for the dependencies of the controls. When the controls assembly is copied for use in another project, all the dependencies are deployed with it as they are all bundled in the one dll file.

The next question that should be ringing around your head is how do these decencies get requested and rendered to the browser? Here is where our friend IHttpHandler comes into play. When a class implements the IHttpHandler interface, it is forced to support the ProcessRequest method. The code in this method can determine the resource being requested by the browser and take appropriate action. In the case of our dependencies, we will determine which resource is requested, extract it from the assembly and then render it to the browser.

Using IHttpHandler is perfect for covering our dependency deployment problems, but there is a just a little configuration that needs to be done in the web site to get it working. The web.config file allows us to specify a page/url reference that will cause our IHttpHandler to be called into action. This configuration causes the server to call our IHttpHandler implementing class when a specific url is requested. Just in case you are a little confused about this, the page requested doesn’t actually exist, but the browser can still request it. When this happens, instead of returning a 404 error, ASP.Net hijacks the request and passes it on to the handler.

This is what is required in the web.config file:

<configuration >
    <system.web >
        <httpHandlers >
            <add  verb ="*"  path ="MyDev.Web.Controls.aspx"  type ="MyDev.Web.Controls.ResHandler,MyDev.Web.Controls" />
        </httpHandlers >
    </system.web >
</configuration >

We have now been able to hook up a web request to call the IHttpHandler interface in a specific class and assembly. Now when the request comes through, we need to identify which resource in the assembly is being requested.When I was originally researching this, I came across an article by Eric Woodruff that had a great idea for not only identifying the resource requested, but also supporting a caching flag. It will be through querystrings that the handler will be told what resource is being requested.

In the previous article, I discussed the best ways of rendering scripts for a control. Most control developers render the script directly to the page either when the page starts rendering, or when the page finishes rendering or when the control renders. As discussed, these scripts can be pulled out of the assembly for easier development, but rendering the script directly in the page doesn’t allow for any caching of the data. With this method, a control could render a script tag that looks like <SCRIPT type='text/javascript' src='MyDev.Web.Controls.aspx?Res=MyControlScript.js' />. This causes the browser to make a separate request for the script which can now be cached by the browser. The result is that the pages will load much faster.

I created a class that implements IHttpHandler and added as many helpful features as I could think of. It will handle text and binary resources and make its best determination as to the content-type of the resource. It also supports some useful methods for registering resources with a page. As this class deals with reflection, assemblies and resources, I have cached objects and data that are often referenced and minimised the number of times the same method is called when processing the resources. Let me know if you find more improvements that can be made.

Here are the goods:

# Region " Imports "

 Imports System
 Imports System.Web

# End  Region

Public  Class ResHandler

# Region " Declarations "

Implements IHttpHandler

Public  Structure TResType

    Public PerceivedType As  String

    Public ContentType As  String

End  Structure

Private m_objAssembly As Reflection.Assembly

Private m_sAssemblyName As  String

# End  Region

# Region " Sub Procedures "

Private  Sub LoadTypeFromRegistry( _
    ByVal sFileExt As  String , _
    ByRef objType As TResType)

    Dim objKey As Microsoft.Win32.RegistryKey = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey("." & sFileExt, False )

    ' Check if the Content Type exists
    If  Not objKey.GetValue("Content Type") Is  Nothing  Then

        objType.ContentType = CType (objKey.GetValue("Content Type"), String )

    End  If  ' End checking if the Content Type exists

    ' Check if the PerceivedType exists
    If  Not objKey.GetValue("PerceivedType") Is  Nothing  Then

        ' Store the PerceivedType value
        objType.PerceivedType = CType (objKey.GetValue("PerceivedType"), String )

    End  If  ' Check if the PerceivedType exists

    ' Check if the perceived type doesn't exist, but can be determined from the content type
    If objType.PerceivedType = vbNullString _
        AndAlso objType.ContentType <> vbNullString _
        AndAlso objType.ContentType.IndexOf("/") > -1 Then

        objType.PerceivedType = objType.ContentType.Substring(0, objType.ContentType.IndexOf("/"))

    End  If  ' End checking if the perceived type doesn't exist, but can be determined from the content type

End  Sub

Private  Sub LoadTypeFromName( _
    ByVal sFileExt As  String , _
    ByRef objType As TResType)

    Select  Case sFileExt

        Case "mid", "midi", "mp3", "wav", "wma"

            ' Toggle sfileext as appropriate for storage
            If sFileExt = "mp3" Then sFileExt = "mpeg"

            If sFileExt = "wma" Then sFileExt = "x-ms-wma"

            objType.PerceivedType = "audio"

        Case "csv", "xls", "doc", "dot", "hta", "ppt", "zip", "tgz"

            ' Toggle sfileext as appropriate for storage

            If sFileExt = "csv" OrElse sFileExt = "xls" Then sFileExt = "vnd.ms-excel"

            If sFileExt = "doc" OrElse sFileExt = "dot" Then sFileExt = "msword"

            If sFileExt = "ppt" Then sFileExt = "vnd.ms-powerpoint"

            If sFileExt = "zip" Then sFileExt = "x-zip-compressed"

            If sFileExt = "tgz" Then sFileExt = "x-compressed"

            objType.PerceivedType = "application"

        Case "bmp", "gif", "jpg", "jpeg", "png", "tiff", "tif", "ico"

            ' Toggle sfileext as appropriate for storage
            If sFileExt = "ico" Then sFileExt = "x-icon"

            If sFileExt = "tif" Then sFileExt = "tiff"

            objType.PerceivedType = "image"

        Case "mht", "mhtml"

            ' Toggle sfileext as appropriate for storage
            sFileExt = "rfc822"

            objType.PerceivedType = "message"

        Case "txt", "tab", "vb", "vbs", "js", "xml", "xsl", "htc", "htm", "html"

            ' Toggle sfileext as appropriate for storage
            If sFileExt = "vb" OrElse sFileExt = "vbs" Then sFileExt = "vbscript"

            If sFileExt = "js" Then sFileExt = "javascript"

            If sFileExt = "txt" OrElse sFileExt = "tab" Then sFileExt = "plain"

            If sFileExt = "xsl" Then sFileExt = "xml"

            If sFileExt = "htc" Then sFileExt = "x-component"

            If sFileExt = "htm" Then sFileExt = "html"

            objType.PerceivedType = "text"

        Case "avi", "mov", "mpe", "mpeg"

            ' Toggle sfileext as appropriate for storage
            If sFileExt = "mov" Then sFileExt = "quicktime"

            If sFileExt = "mpe" Then sFileExt = "mpeg"

            objType.PerceivedType = "video"

        Case  Else

            ' Default to binary data
            objType.PerceivedType = "application"

            sFileExt = "octet-stream"

    End  Select

    ' Create the content type from the perceived type and the file extension
    objType.ContentType = objType.PerceivedType & "/" & sFileExt

End  Sub

Public  Sub RegisterWithPage( _
    ByVal sResource As  String , _
    ByRef objPage As UI.Page, _
    Optional  ByVal bRegisterAsStartupScript As  Boolean = False )

    ' Check if the resource is valid
    If IsValid(sResource) = True  Then

        ' Check if we should register the resource
        If (((bRegisterAsStartupScript = False ) _
            AndAlso (objPage.IsClientScriptBlockRegistered(sResource) = False )) _
            OrElse ((bRegisterAsStartupScript = True ) _
            AndAlso (objPage.IsStartupScriptRegistered(sResource) = False ))) Then

            ' Check if we can get a page reference
            Dim sReference As  String = PageReference(sResource)

            ' Check if there is a reference to use
            If sReference <> vbNullString Then

                ' Check if the reference is a startup script
                If bRegisterAsStartupScript = True  Then

                    ' Register the resource
                    objPage.RegisterStartupScript(sResource, sReference)

                Else  ' This is not a startup script

                    ' Register the resource
                    objPage.RegisterClientScriptBlock(sResource, sReference)

                End  If  ' End checking if the reference is a startup script

            End  If  ' End checking if there is a reference to use

        End  If  ' End checking if we should register the resource

    End  If  ' End checking if the resource is valid

End  Sub

Public  Sub ProcessRequest( _
    ByVal context As HttpContext) _
    Implements IHttpHandler.ProcessRequest

    ' Clear any existing response data

    context.Response.Clear()

    Try

        Dim sResource As  String = context.Request.QueryString("Res")
        Dim bSuccess As  Boolean

        ' Check if the resource request is valid
        If IsValid(sResource) = True  Then

            Dim bNoCache As  Boolean = False
            Dim tType As TResType = ResourceType(sResource)

            ' Check if NoCache has been specified
            If context.Request.QueryString("NoCache") <> vbNullString Then

            Try

                ' Get the cache value
                bNoCache = System.Boolean.Parse(context.Request.QueryString("NoCache"))

            Catch

                ' Do nothing

            End  Try

            End  If  ' End checking if NoCache has been specified

            With context.Response

                ' Set up the response
                .ContentType = tType.ContentType
                .Cache.VaryByParams("Res") = True
                .Cache.VaryByParams("NoCache") = True
                .Cache.SetValidUntilExpires( False )

                ' Check if caching is enabled
                If bNoCache = False  Then

                    .Cache.SetExpires(Now.AddDays(7))
                    .Cache.SetCacheability(HttpCacheability.Public)

                Else  ' Caching is disabled

                    .Cache.SetExpires(Now.AddDays(-7))
                    .Cache.SetCacheability(HttpCacheability.NoCache)

                End  If  ' End checking if caching is enabled

                ' Check if the type of resource is text
                If tType.PerceivedType = "text" Then

                    Dim sResourceValue As  String

                    ' Check if the resource was successfully loaded
                    If LoadTextResource(sResource, sResourceValue) = True  Then

                        ' Write the value to the browser
                        .Write(sResourceValue)

                        ' Set the success flag
                        bSuccess = True

                    End  If  ' End checking if the resource was successfully loaded

                Else  ' This resource is binary

                    Dim aBuffer As  Byte ()

                    ' Check if the resource was successfully loaded
                    If LoadBinaryResource(sResource, aBuffer) = True  Then

                        ' Write the value to the browser
                        .OutputStream.Write(aBuffer, 0, aBuffer.Length)

                        ' Set the success flag
                        bSuccess = True

                    End  If  ' End checking if the resource was loaded successfully

                End  If  ' End checking if the type of resource is text

            End  With  ' End With context.Response

        End  If  ' End checking if the resource request is valid

        ' Check if the resource failed to load
        If bSuccess = False  Then

            With context.Response

                .Cache.SetExpires(Now.AddDays(-7))
                .Cache.SetCacheability(HttpCacheability.NoCache)
                .Status = "The resource requested doesn't exist."
                .StatusCode = 404
                .StatusDescription = .Status

            End  With  ' End With context.Response

        End  If  ' End checking if the resource failed to load

    Catch ex As Exception

        With context.Response

            .Cache.SetExpires(Now.AddDays(-7))
            .Cache.SetCacheability(HttpCacheability.NoCache)
            .Status = ex.Message
            .StatusCode = 500
            .StatusDescription = .Status

        End  With  ' End With context.Response

    End  Try

End  Sub

# End  Region

# Region " Functions "

Public  Function IsValid( _
    ByRef sResource As  String ) As  Boolean

    ' Exit if no value is specified
    If sResource = vbNullString Then  Return  False

    Dim aResources() As  String = ThisAssembly.GetManifestResourceNames
    Dim sCheckName As  String = sResource.ToLower
    Dim nCount As Int32

    ' Add the assembly namespace to the resource name if it isn't already
    If sCheckName.StartsWith(AssemblyName.ToLower) = False  Then sCheckName = AssemblyName.ToLower & "." & sCheckName

    ' Loop through the resource names
    For nCount = 0 To aResources.Length - 1

        ' Check if the resource name matches
        If sCheckName = aResources(nCount).ToLower Then

            ' Update the resource parameter
            sResource = aResources(nCount)

            ' The resource was found
            Return  True

        End  If  ' End checking if the resource name matches

    Next  ' Loop through the resource names

    ' The resource wasn't found
    Return  False

End  Function

Public  Function URL( _
    ByVal sResource As  String , _
    Optional  ByVal bNoCache As  Boolean = False ) As  String

    Dim sURL As  String = AssemblyName & ".aspx?Res=" & HttpUtility.UrlEncode(sResource)

    ' Check if NoCache is specified
    If bNoCache = True  Then

        sURL &= "&NoCache=" & bNoCache

    End  If  ' End checking if NoCache is specified

    ' Return the URL built
    Return sURL

End  Function

Public  Function PageReference( _
    ByVal sResource As  String ) As  String

    Dim tType As TResType = ResourceType(sResource)

    If tType.PerceivedType = "image" Then

        Return "<IMG src='" & URL(sResource) & "'></IMG>"

    ElseIf tType.ContentType = "text/css" Then

        Return "<LINK rel='Stylesheet' href='" & URL(sResource) & "'></LINK>"

    ElseIf tType.ContentType = "text/javascript" Then

        Return "<SCRIPT language='javascript' type='text/javascript' src='" & URL(sResource) & "'></SCRIPT>"

    ElseIf tType.ContentType = "text/vbscript" Then

        Return "<SCRIPT language='vbscript' type='text/vbscript' src='" & URL(sResource) & "'></SCRIPT>"

    ElseIf tType.PerceivedType = "text" Then

        Dim sValue As  String

        ' Check if the resource was loaded successfully
        If LoadTextResource(sResource, sValue) = True  Then

            ' Return the resource value
            Return sValue

        Else  ' The resource wasn't loaded successfully

            Return vbNullString

        End  If  ' End checking if the resource was loaded successfully

    Else  ' This type of resource is not handled

        Return vbNullString

    End  If

End  Function

Public  Function ResourceType( _
    ByVal sResource As  String ) As TResType

    ' Check if there is a .
    If sResource <> vbNullString _
        AndAlso sResource.LastIndexOf(".") > -1 Then

        ' Get the file extension
        Dim sFileExt As  String = sResource.Substring(sResource.LastIndexOf(".") + 1).ToLower
        Dim objType As TResType

        ' Determine the file type from the registry
        Call LoadTypeFromRegistry(sFileExt, objType)

        ' Check if there is any type information missing
        If objType.PerceivedType = vbNullString _
            OrElse objType.ContentType = vbNullString Then

            Dim objTempType As TResType

            ' Get the type from the name
            Call LoadTypeFromName(sFileExt, objTempType)

            ' Store the values as appropriate
            If objType.ContentType = vbNullString AndAlso objTempType.ContentType <> vbNullString Then objType.ContentType = objTempType.ContentType

            If objType.PerceivedType = vbNullString AndAlso objTempType.PerceivedType <> vbNullString Then objType.PerceivedType = objTempType.PerceivedType

        End  If  ' End checking if there is any type information missing

        ' Return the type information determined
        Return objType

    Else  ' An invalid value has been specified

        Return  Nothing

    End  If  ' End checking if there is a .

End  Function

Public  Function LoadTextResource( _
    ByVal sResource As  String , _
    ByRef sValue As  String ) As  Boolean

    Dim bReturn As  Boolean

    ' Check if the resource name is valid
    If IsValid(sResource) = True  Then

        Dim tType As TResType = ResourceType(sResource)

        ' Check if the resource is a text resource

        If tType.PerceivedType = "text" Then

            Dim objStream As IO.Stream = ThisAssembly.GetManifestResourceStream(sResource)

            Dim objReader As  New IO.StreamReader(objStream)

            Try

                ' Read the resource stream
                sValue = objReader.ReadToEnd

                ' We have loaded the resource successfully, set the return flag
                bReturn = True

            Catch

                ' Do nothing

            Finally

                ' Check if the reader exists
                If  Not objReader Is  Nothing  Then

                    ' Close the reader
                    objReader.Close()

                End  If  ' End checking if the reader exists

                ' Check if the stream exists

                If  Not objStream Is  Nothing  Then

                    ' Close the stream
                    objStream.Close()

                End  If  ' End checking if the stream exists

            End  Try

        End  If  ' End checking if the resource is a text resource

    End  If  ' End checking if the resource name is valid

    ' Return the success flag

    Return bReturn

End  Function

Public  Function LoadBinaryResource( _
    ByVal sResource As  String , _
    ByRef aBuffer As  Byte ()) As  Boolean

    Dim bReturn As  Boolean

    ' Check if the resource name is valid
    If IsValid(sResource) = True  Then

        Dim tType As TResType = ResourceType(sResource)

        ' Check if the resource is a text resource
        If tType.PerceivedType <> "text" Then

            Dim objStream As IO.Stream = ThisAssembly.GetManifestResourceStream(sResource)

            Dim objReader As  New IO.BinaryReader(objStream)

            Try

                ' Resize the buffer to fit the resource
                ReDim aBuffer( CType (objStream.Length, Int32))

                ' Load the resource into the buffer
                objReader.Read(aBuffer, 0, CType (objStream.Length, Int32))

                ' We have loaded the resource successfully, set the return flag
                bReturn = True

            Catch

                ' Do nothing

            Finally

                ' Check if the reader exists
                If  Not objReader Is  Nothing  Then

                    ' Close the reader
                    objReader.Close()

                End  If  ' End checking if the reader exists

                ' Check if the stream exists
                If  Not objStream Is  Nothing  Then

                    ' Close the stream
                    objStream.Close()

                End  If  ' End checking if the stream exists

            End  Try

        End  If  ' End checking if the resource is a text resource

    End  If  ' End checking if the resource name is valid

    ' Return the success flag

    Return bReturn

End  Function

# End  Region

# Region " Properties "

Private  ReadOnly  Property ThisAssembly() As System.Reflection.Assembly
    Get

        ' Check if there is an assembly reference
        If m_objAssembly Is  Nothing  Then

            ' Get a reference to the assembly
            m_objAssembly = System.Reflection.Assembly.GetExecutingAssembly

        End  If  ' End checking if there is an assembly reference

        ' Return the object reference
        Return m_objAssembly

    End  Get

End  Property

Private  ReadOnly  Property AssemblyName() As  String
    Get

        ' Check if the value is stored
        If m_sAssemblyName = vbNullString Then

            m_sAssemblyName = Me .GetType.Namespace

        End  If  ' End checking if the name is stored

        ' Return the stored value
        Return m_sAssemblyName

    End  Get

End  Property

Public  ReadOnly  Property IsReusable() As  Boolean  Implements IHttpHandler.IsReusable

    Get

        Return  True

    End  Get

End  Property

# End  Region

End  Class
Written on April 11, 2005