|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionWhen 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 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 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.0The 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 Add resources to your projectTo 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 projectAdd 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 // 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 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 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, /// <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 /// <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 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 // 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 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 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 controlUsing 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 // 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 <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 applicationIn 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 <!-- 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 Allowing anonymous access to resources when using forms-based authenticationWhen 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 <!-- 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 Common errors and problemsThe 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 " 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 demoTo 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. ConclusionI 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
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||