用于自定义控件的资源服务器处理程序类

分享于 

32分钟阅读

Web开发

  繁體 雙語

介绍

为 ASP.NET 开发自定义控件时,可能需要创建一些客户端脚本,用于与自定义控件交互。 还可以能有用于控件的某些元素的图像文件,例如设置控件外观的按钮或者样式表。 有关如何使用自定义控件程序集部署这些资源的决策。

脚本可以使用 StringBuilder 或者 static 字符串构建,并使用 RegisterClientScriptBlock 直接插入到页面中。 这对小脚本很适用,但是对于大型脚本来说很笨糙,尤它的是如果它们足够需要调试来解决问题。 脚本也可以作为资源嵌入程序集中,在运行时检索,然后使用 RegisterClientScriptBlock 再次插入到页面中。 这比 StringBuilder 方法更好,但是它仍然插入到页面中,并且每次请求页面时都会呈现。 你拥有的脚本代码或者页面上呈现支持脚本代码的页面越多,页面越大。 脚本也不能缓存在客户端上,以节省后续请求的时间。

这些脚本可以作为单独的文件一起分发到程序集。 这解决了在每个请求页上呈现的代码的问题,并且可以在客户端缓存。 但是,它可能会使自定义控件的分发复杂化。 现在它不再是一个简单的XCOPY部署,因为现在必须随程序集一起安装脚本。 如果应用程序是使用 SSL,还是如何设置最终用户的应用程序,可能会影响多个副本。 如果在未来的控件版本中修改了脚本,则版本控制问题也可以能发生。

为了解决这些问题,我开发了一个实现 System.Web.IHttpHandler 接口的类,并充当了排序的资源服务器。 我看到的示例通过示例显示了如何使用ASPX页面作为 image 标记来呈现动态映像。 这个概念对于资源服务器处理程序基本上是相同的。 将资源嵌入控件程序集中,然后向实现 IHttpHandler 接口的自定义控件程序集中添加一个简单的类。 向应用程序文件的Web.Config 中添加一个节,以将资源请求直接添加到处理程序类。 处理器类使用查询字符串中的参数来确定内容类型,如脚本。图像。样式表或者它的他类型。

通过在程序集中嵌入资源并根据需要服务它们,在页面中尽可以能少地呈现代码。 也可以在客户端上缓存资源处理程序响应,因这里可以在使用相同资源的后续页请求中提高性能。 对于拨号连接较慢的用户,尤其是在使用启用了自动回发的控件的窗体上,这是最有益的。 这些资源不必与程序集分开部署,从而解决安装资源的问题以及版本控制问题。 我们又回到了一个简单的XCOPY部署。

资源服务器处理程序的使用不限于自定义控件。 它还增加了使用客户端脚本来完成诸如请求动态内容之类的事情的能力。 例如可以使用客户端脚本将数据库查询的结果检索为 XML。 结果可以用于在需要时填充控件或者弹出窗口,而不是在第一次加载时发送所有内容。 下面的部分描述如何设置和利用资源服务器处理程序类。

关于 ASP.NET 2.0的单词

以下将允许你在 ASP.NET 1.1应用程序中嵌入和服务资源,以及 ASP.NET 2.0应用程序。 然而,ASP.NET 2.0提供嵌入式网络资源的能力是一个内置的特性,更易于实现。 它利用了嵌入式资源,但利用属性来定义它们和 Page.ClientScript 方法来呈现链接到客户端。 函数不要求编写处理程序,也不要求 Web.Config 文件中的任何条目定义处理程序或者允许匿名访问它们。 他写了一篇很好的文章,所以我只想让你参考一下它,而不是重复它所说的内容。

向项目中添加资源

要整理内容,请将资源存储在按类型( 脚本文件,图像文件的图像,等等 ) 分组的单独文件夹中。 若要在项目中新建文件夹,请右键单击项目 NAME,选择 Add。 ,选择新文件夹,然后输入文件夹 NAME。 右键单击文件夹,然后选择添加,将新资源添加到文件夹中。 然后添加新项目。 创建新项或者添加现有项。 如果将现有文件复制到新文件夹。 添加到项目文件夹后,右键单击该文件并选择 Properties"。 将生成操作属性更改为嵌入资源。 这里步骤最重要,因为它指示希望将文件作为已经编译的程序集中的资源嵌入。

将ResSrvHandler类添加到项目中

将 ResSrvHandler.cs 源文件添加到控件的项目中,并按照如下方式。 TODO: 添加了注释以帮助你找到需要修改的部分。

修改命名空间,使它的为自定义控件的MATCHES:

// TODO: Change the namespace to match your control's namespace.namespace ResServerTest.Web.Controls
{

修改 cResSrvHandlerPageName 常量以便它与将在应用程序文件的Web.Config 中使用的名称匹配,以便将资源请求直接用于类。 我选择了将自定义控件命名空间与. aspx 扩展一起使用。 这将保持它的独特并保证它不会与最终用户应用程序中的某些内容冲突:

// 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>publicconststring cResSrvHandlerPageName =
 "ResServerTest.Web.Controls.aspx";

修改 cImageResPathcScriptResPath 常量以指向你的脚本和图像路径。 根据需要为其他资源类型路径添加其他常量。 通过使用项目的默认命名空间和资源的文件夹路径创建程序集中嵌入资源的名称。 默认命名空间通常与程序集 NAME 相同,但你可以通过右键单击项目 NAME。 对于演示,默认名称空间已经更改为 MATCH,ResServerTest.Web.Controls的命名空间,和资源路径是图像和脚本。 因此,常数定义如下所示。 还应该包含尾随的"。"。 当从程序集加载资源 NAME 时,它将被追加到适当的常数。

如果使用 VB.Net,则编译器的默认行为与 C# 编译器的默认行为不同。 除非显式包含 命令行 选项,否则不将默认命名空间追加到资源文件名的前面。 因此,对于 VB.Net 项目,可以省略路径常量或者将它的设置为空字符串:

// 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>privateconststring cImageResPath =
 "ResServerTest.Web.Controls.Images.";///<SUMMARY>/// The path to the script resources///</SUMMARY>privateconststring cScriptResPath =
 "ResServerTest.Web.Controls.Scripts.";

调用 ResourceUrl 方法可以格式化用于从程序集中检索嵌入资源的URL。 第一个版本将从包含类的程序集中检索命名资源。 只要将资源的NAME 传递给它,它就会返回指向资源的URL。

方法的第二个版本可以用于从除包含资源服务器类的程序集之外的程序集提取嵌入资源。 将包含资源(。没有路径或者扩展名,例如 System.Web )的程序集的NAME 传递给可以检索它的(。例如,cResSrvHandlerPageName 常量中定义的类)的资源处理程序的NAME 和资源的NAME。 使用这里版本时,资源的NAME 将与以指定的NAME 结束的第一个资源匹配。 如果不了解路径 NAME,或者从不存储路径的VB.Net 程序集中提取资源,则可以跳过路径:

///<summary>/// This can be called to format a URL to a resource name that is/// embedded within the assembly.///</summary>///<paramname="strResourceName">The name of the resource</param>///<paramname="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>publicstaticstring ResourceUrl(string strResourceName,
 bool bCacheResource)
{
 returnString.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>///<paramname="strAssemblyName">The name of the assembly that/// contains the resource</param>///<paramname="strResourceHandlerName">The name of the resource/// handler that can retrieve it (i.e. the ASPX page name)</param>///<paramname="strResourceName">The name of the resource</param>///<paramname="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>publicstaticstring ResourceUrl(string strAssemblyName,
 string strResourceHandlerName, string strResourceName,
 bool bCacheResource)
{
 returnString.Format("{0}?Assembly={1}&Res={2}{3}",
 strResourceHandlerName,
 HttpContext.Current.Server.UrlEncode(strAssemblyName),
 strResourceName, (bCacheResource)? "" : "&NoCache=1");
}

实现 IHttpHandler.IsReusable 属性表示可以为其他请求重用对象实例。 实现 IHttpHandler.ProcessRequest 方法来完成所有工作。 第一步是确定请求的资源的NAME 及其类型。 我使用扩展名的扩展名来确定类型。 代码假定查询字符串参数被称为 Res。 如果选择了其他参数 NAME,请调整。 同样,你可以修改代码以根据需要确定资源 NAME 并键入任意数量的方法:

///<summary>/// Load the resource specified in the query string and return/// it as the HTTP response.///</summary>///<paramname="context">The context object for the/// request</param>publicvoid 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();

下一步是清除任何当前响应并设置缓存选项。 如果未指定 NoCache 查询字符串参数,则类将在 context.Response.Cache 对象中设置必要的页缓存选项。 如果已经指定,则设置选项,以便响应永远不会被缓存。 类默认为缓存了某一天的响应。 根据控件的需要调整这里选项。 响应被设置为通过参数 NAME 改变高速缓存。 默认类只有一个名为 Res的参数。 如果有其他参数,请确保将它们添加为其他 VaryByParams 条目:

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);
}

下一节检查资源是否驻留在另一个程序集中。 如果省略 Assembly 查询字符串选项,则假定资源与类位于同一程序集中。 如果指定,则查找命名程序集,如果找到,则搜索它的清单中的资源。 从其他程序集中加载时,忽略内部类路径名,而使用搜索过程中匹配的NAME:

// 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)
 thrownew 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;
 }
}

就像所给的,这个类可以提供各种图像和脚本类型,演示的一些样式,还有一个附加的XML文件来演示 NoCache 选项。 一个简单 switch 语句用于确定要返回的类型。 相应设置 context.Response.ContentType 属性,检索资源,然后将它的写入响应流。 你可以扩展或者减少代码以满足你的需要:

switch(strType)
{
 case"gif": // Image typescase"jpg":
 case"jpeg":
 case"bmp":
 case"png":
 case"tif":
 case"tiff":
 if(strType == "jpg")
 strType = "jpeg";
 elseif(strType == "png")
 strType = "x-png";
 elseif(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 typescase"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 htmlcase"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 typethrownew Exception("Unknown resource type");
}

对于基于文本的简单资源( 如脚本),可以使用 StreamReader.ReadToEnd 方法检索资源。 对于像图像这样的二进制资源,必须分配一个 array 并使用 StreamReader.Read 将图像加载到 array。 加载之后,你就可以将 array 写到客户端上,如下所示。

如果请求未知资源类型,或者无法从程序集中加载,则会引发异常。 对于脚本资源类型,异常处理程序将将响应转换为适当的类型并发回消息框或者警报。 这将给你一个在开发过程中失败的机会。 对于XML资源,异常处理程序将返回包含资源 NAME 和错误描述的节点的XML响应。 对于所有其他资源类型,没有返回任何内容。 图像将显示一个破坏的图像占位符,这可以表示你可以能已经做了错误的事情:

catch(Exception excp)
{
 XmlDocument xml;
 XmlNode node, element;
 string strMsg = excp.Message.Replace("rn", "");
 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();
}

在控件中使用资源服务器处理程序

使用自定义控件中的资源服务器处理程序非常简单。 只要向类添加代码即可呈现属性。脚本标记或者其他资源类型,如使用资源服务器页面 NAME的图像。 通过在 ResSrvHandler.ResourceUrl 方法中调用资源名和一个布尔标志来实现这一点,该方法指示是否在客户端上缓存该方法。 演示控件包含几个示例。

// An imageimg = 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 belowimg.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");

如前所述,缺少 NoCache 查询字符串选项将导致在客户端上缓存资源。 若要关闭资源的缓存,只需指定 false 对于 ResourceUrl 方法的缓存参数,或者将 NoCache 参数添加到查询字符串中,如果手动编码 URL。 demo页包含一个示例,该示例从控件程序集检索一个XML文档。 它使用无缓存选项,以便在每次请求XML资源时显示服务器上的当前时间。 它还包含了几个示例,从自定义程序集的自定义程序集检索资源。

<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>

在应用程序中使用控件和资源服务器处理程序

在应用程序项目中,添加对自定义控件的引用并将自定义控件添加到应用程序的形式。 若要使用资源服务器处理程序,请在 <system.web> 你的应用程序文件的Web.Config 部分,如下所示:

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

修改 path 属性,使它的MATCHES ResSrvHandler.cResSrvHandlerPageName 常量。修改 type 属性以引用处理程序 NAME ( 包括它的命名空间),后面跟逗号,然后是程序集的NAME。 这里项导致任何包含在 path 属性中指定的页面 NAME的请求,无论文件夹如何映射到资源处理程序类。

允许匿名访问在使用基于表单的身份验证时的资源

当基于表单身份验证保护整个应用程序时,登录页上的资源被阻止,因为上述HTTP处理程序使用ASPX页面名称将请求路由到处理程序。 因这里,它像对普通页面的任何它的他请求一样,而不是返回资源,ASP.NET 将请求重定向到登录页。 为了防止这种情况,允许从登录页面匿名访问资源服务器处理器,应该将以下部分添加到 Web.Config file:的<configuration> 部分。

<!-- 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. --><locationpath="ResServerTest.Web.Controls.aspx"><system.web><authorization><allowusers="*"/></authorization></system.web></location>

这允许所有用户访问没有身份验证的资源,因这里允许使用资源服务器处理程序在登录web窗体。 在 location 标签的path 属性中修改页面名称以匹配在HTTP处理程序部分中使用的页面。

常见错误和问题

使用资源服务器处理程序时最常见的错误是将资源 NAME 拼错,忘记将生成操作属性更改为嵌入资源。 在这两种情况下" 值不能为空。参数 NAME: 溪流 "为脚本和XML资源返回。 图像资源将无法显示任何内容。 演示项目包含这些错误的示例。

在 Web.Config 文件或者Web窗体中引用动态内容时,在文件中引用该页名时,另一个常见错误是。 在这些情况下,你会得到链接或者"找不到资源"错误。 如果在 Web.Config 文件中拼错了类或者程序集 NAME,应用程序将无法启动,告诉你它无法找到指定的类型或者程序集。 错误消息中显示的类型或者程序集将是资源处理程序类或者它的程序集的错误 NAME。 更正这些名称将解决错误。

开发过程中可以能发生的问题是对资源进行修改,然后在测试控件时看到这些变化。 原因是嵌入的资源不会创建生成依赖项。 因此,修改它们并不会导致程序集的重新生成。 进行此类更改时,只需记住始终强制强制重新生成程序集,以便它嵌入已经更新的资源。 你还可能不得不强制刷新页面,以便下载更新的资源( IE 中的Ctrl+F5示例)。

通过打开浏览器并向资源输入 URL,你可以测试资源的检索并查看返回的内容。 对于基于文本的资源,用"查看源:"前缀 URL。 例如:

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

这将检索 DemoCustomControl.js 文件并在记事本窗口中显示它。

演示

要试用演示应用程序和定制控件,在IIS中创建一个虚拟目录,并将它的指向文件夹。 启动页面是 WebForm1.aspx. demo演示项目设置为在具有 Visual Studio. NET 2003和运行在它的上的的开发机器上编译和运行。 如果你使用的是远程服务器,则需要设置虚拟目录,分别构建示例控件并将演示应用程序文件复制。

结束语

我在自定义控件中使用了这种方法,它使用了客户端脚本和图像文件,并且发现它非常有用。 随着控制程序集的部署,涉及客户端脚本的控件的开发变得更加简单。

修订历史

  • 04/02/2006
    • 删除了 ReadBinaryResource,因为它不需要。 还添加了关于 ASP.NET 2.0中的web资源新支持的说明。
  • 06/27/2004

    进行了一些代码调整。根据 Adam Pawsey的建议进行了以下添加:

    • 添加了一些 static helper 方法来创建资源链接。
    • 除了包含资源处理程序类之外,还增加了从其他程序集加载资源的支持。
  • 07/19/2003
    • 添加了一部分,用于使资源服务器使用表单的身份验证从应用程序中的登录web窗体。 更新了XML代码文档。 使用 Visual Studio. NET 2003重新生成项目。
  • 04/09/2003
    • 更改了资源服务器类,以便它实现 System.Web.IHttpHandler 而不是从 System.Web.UI.Page 派生。 这将提高性能并进一步简化类的使用。
  • 04/06/2003
    • 初始版本。