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






4.78/5 (41投票s)
实现 IHttpHandler 接口的类,用于自定义控件,将脚本、图像和样式表等嵌入资源发送到客户端。
引言
在开发 ASP.NET 自定义控件时,可能需要创建一些客户端脚本来与自定义控件进行交互。还可能需要一些图像文件用于控件的某些元素,例如按钮或用于设置控件外观的样式表。必须决定如何将这些资源与自定义控件程序集一起部署。
可以使用 StringBuilder
或静态字符串来构建脚本,并使用 RegisterClientScriptBlock
直接插入到页面中。这对于小型脚本来说还可以,但对于较大的脚本来说会很不方便,特别是当它们足够复杂以至于在开发过程中需要调试来解决问题时。脚本也可以作为资源嵌入到程序集中,在运行时检索,然后再次使用 RegisterClientScriptBlock
插入到页面中。这比 StringBuilder
方法更适合大型脚本,但它仍然会被插入到页面中,并且每次请求页面时都会完全呈现。您拥有的脚本代码越多,或者页面上渲染支持脚本代码的控件越多,页面就会越大。脚本也无法在客户端缓存以节省后续请求的时间。
脚本可以作为独立文件与程序集一起分发。这解决了每次请求时代码都在页面中渲染的问题,并且可以在客户端缓存。但是,这可能会使自定义控件的部署复杂化。它不再是简单的 XCOPY 部署,因为现在必须与程序集一起安装脚本。许多因素,例如是生产服务器还是开发服务器,应用程序是否使用 SSL,以及最终用户的应用程序如何设置,都会影响脚本的存放位置,您最终可能会在多个位置拥有多个副本。如果在控件的未来版本中修改了脚本,版本控制问题也可能出现。
为了解决这些问题,我开发了一个实现 System.Web.IHttpHandler
接口的类,并充当一种资源服务器。这个想法受到了一些示例的启发,这些示例展示了如何使用 ASPX 页面作为 image
标签的源来渲染动态图像。对于资源服务器处理程序,概念基本相同。您将资源嵌入到控件程序集中,然后将一个简单的类添加到您的自定义控件程序集中,该类实现 IHttpHandler
接口。在应用程序的 Web.Config 文件中添加一个部分,将资源请求定向到处理程序类。处理程序类使用查询字符串中的参数来确定内容类型,并发送适当的资源,如脚本、图像、样式表或您需要的任何其他类型的数据。
通过将资源嵌入到程序集中并按需提供,控件在页面中呈现的代码量最少。资源处理程序的响应也可以在客户端缓存,因此在后续使用相同资源的页面请求中,可以提高性能,因为发送到客户端的信息更少。这对于连接速度较慢(例如拨号连接)的用户来说最有益,特别是对于启用了自动回发(auto-postback)的控件的表单。资源也不必与程序集分开部署,从而解决了资源安装位置的问题以及任何版本控制问题。我们又回到了简单的 XCOPY 部署。
资源服务器处理程序的用途不限于自定义控件。它还增加了执行诸如使用客户端脚本请求动态内容(如 XML)的功能。例如,可以使用客户端脚本发出请求以从数据库查询中检索 XML 结果。这些结果可用于在需要时填充控件或弹出窗口,而不是在首次加载页面时发送所有内容。以下各节将介绍如何设置和使用资源服务器处理程序类。
关于 ASP.NET 2.0 的说明
以下内容将允许您在 ASP.NET 1.1 应用程序以及 ASP.NET 2.0 应用程序中嵌入和提供资源。但是,在 ASP.NET 2.0 中,提供嵌入式 Web 资源的功能是内置的,并且实现起来更简单。它利用此处描述的嵌入式资源,但使用属性来定义它们,并使用内置的 Page.ClientScript
方法向客户端呈现指向它们的链接。它不需要您编写处理程序,也不需要 Web.config 文件中的任何条目来定义处理程序或允许匿名访问它们。Gary Dryden 已经写了一篇关于此工作的优秀文章,我将在此引用它,而不是重复他的内容(WebResource ASP.NET 2.0 Explained[^])。
将资源添加到您的项目
为了保持组织性,请将资源存储在按类型分组的单独文件夹中(Scripts 用于脚本文件,Images 用于图像文件等)。要在项目中创建新文件夹,请右键单击项目名称,选择 Add...,选择 New Folder,然后输入文件夹名称。要向文件夹添加新资源,请右键单击它,然后选择 Add...,然后选择 Add New Item... 来创建新项,或者如果您已将现有文件复制到新文件夹,则选择 Add Existing Item...。添加到项目文件夹后,右键单击文件并选择 Properties。将 Build Action 属性从 Content 更改为 Embedded Resource。此步骤最重要,因为它表示您希望将文件嵌入到编译后的程序集中作为资源。
将 ResSrvHandler 类添加到您的项目
将 ResSrvHandler.cs 源文件添加到您的控件项目中,并按如下方式修改它。已添加 TODO: 注释,以帮助您找到需要修改的部分。
修改命名空间,使其与您的自定义控件的命名空间匹配
// 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>
public const string cResSrvHandlerPageName =
"ResServerTest.Web.Controls.aspx";
修改 cImageResPath
和 cScriptResPath
常量,使其指向您的脚本和图像路径。根据需要为其他资源类型路径添加额外的常量。程序集中嵌入资源的名称是通过使用项目的默认命名空间加上资源的文件夹路径来创建的。默认命名空间通常与程序集名称相同,但您可以通过右键单击项目名称,选择 Properties,然后在 Common Properties 条目的 General 部分更改 Default Namespace 选项来修改它。对于演示,默认命名空间已更改为与控件的命名空间 ResServerTest.Web.Controls
匹配,并且资源路径为 Images 和 Scripts。因此,常量定义如下所示。还应包含尾随的点。加载资源时,资源名称将被追加到相应的常量中。
请注意,如果您使用的是 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>
private const string cImageResPath =
"ResServerTest.Web.Controls.Images.";
/// <SUMMARY>
/// The path to the script resources
/// </SUMMARY>
private const string cScriptResPath =
"ResServerTest.Web.Controls.Scripts.";
可以通过调用 ResourceUrl
方法来格式化用于从程序集中检索嵌入资源的 URL。第一个版本将从包含该类的程序集中检索命名资源。只需将资源名称传递给它,它就会返回指向该资源的 URL。
该方法的第二个版本可用于从包含资源服务器类的程序集之外的程序集中提取嵌入式资源。将包含资源的程序集名称(不带路径或扩展名,例如 System.Web
)、可以检索它的资源处理程序名称(即 cResSrvHandlerPageName
常量中定义的类)以及要检索的资源名称传递给它。使用此版本时,将资源名称与以指定名称结尾的第一个资源进行匹配。这允许您在不知道路径名的情况下跳过路径名,或者提取 VB.NET 程序集中的资源,这些程序集不存储路径。
/// <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");
}
实现了 IHttpHandler.IsReusable
属性,以指示对象实例可以被其他请求重用。实现了 IHttpHandler.ProcessRequest
方法来执行所有工作。第一步是确定请求资源的名称及其类型。我使用文件名扩展名来确定类型。代码假定查询字符串参数名为 Res
。如果选择不同的参数名称,请进行调整。同样,您可以根据您的需求修改代码以用任何方式确定资源名称和类型。
/// <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();
下一步是清除任何当前响应并设置缓存选项。如果未指定 NoCache
查询字符串参数,则该类会在 context.Response.Cache
对象中设置必要的页面缓存选项。如果指定了该参数,则选项设置为永不缓存响应。该类默认将响应缓存一天。根据需要为您的控件进行调整。响应设置为按参数名称缓存。默认类只有一个名为 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
查询字符串选项,则假定资源与该类位于同一程序集中。如果指定了,它会查找命名程序集,如果找到,则在其清单中搜索资源。从不同程序集加载时,将忽略内部类路径名,而是使用搜索期间匹配的名称。
// 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;
}
}
如给定所示,该类可以提供各种图像和脚本类型,一些用于演示的样式,以及一个额外的 XML 文件来演示 NoCache
选项。使用了一个简单的 switch
语句来确定要返回的类型。相应地设置 context.Response.ContentType
属性,检索资源,然后将其写入响应流。您可以根据需要扩展或缩减代码。
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");
}
对于脚本等简单的文本资源,可以使用 StreamReader.ReadToEnd
方法来检索资源。对于图像等二进制资源,必须分配一个数组并使用 StreamReader.Read
将图像加载到数组中。加载后,您可以将数组写入客户端,如上所示。
如果请求了未知资源类型或无法从程序集中加载,则会引发异常。对于脚本资源类型,异常处理程序会将响应转换为适当的类型,并发送一个消息框或警报,以便在页面加载时显示异常。这将为您提供机会在开发过程中看到什么失败了。对于 XML 资源,异常处理程序将发送一个包含资源名称和错误描述节点的 XML 响应。对于所有其他资源类型,不返回任何内容。图像将显示损坏的图像占位符,这表明您可能做错了什么。
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();
}
在控件中使用资源服务器处理程序
在自定义控件中使用资源服务器处理程序非常简单。只需将代码添加到您的类中,以呈现使用资源服务器页面名称的属性、脚本标签或其他资源类型(如图像)。这是通过调用 ResSrvHandler.ResourceUrl
方法,传入资源名称和指示是否在客户端缓存的布尔标志来实现的。演示控件包含几个示例。
// 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");
如前所述,缺少 NoCache
查询字符串选项将导致资源在客户端缓存。要关闭资源的缓存,只需为 ResourceUrl
方法的缓存参数指定 false
,或者在手动编码 URL 时将 NoCache
参数添加到查询字符串中。演示 ASPX 页面包含一个从控件程序集中检索 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>
在应用程序中使用控件和资源服务器处理程序
在应用程序的项目中,添加对自定义控件程序集的引用,然后以常规方式将自定义控件添加到应用程序的窗体中。要使用资源服务器处理程序,请在应用程序的 Web.Config 文件的 <system.web>
部分添加类似以下的条目
<!-- 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>
修改 path
属性,使其与 ResSrvHandler.cResSrvHandlerPageName
常量匹配。修改 type
属性,使其引用您的资源服务器处理程序的类名(包括其命名空间),后面跟着一个逗号,然后是程序集的名称。此条目会导致包含 path
属性中指定的页面名称的任何请求(无论文件夹如何)都被映射到您的资源处理程序类。
在使用基于表单的身份验证时允许匿名访问资源
当使用基于表单的身份验证来保护整个应用程序时,登录页面上的资源访问会被阻止,因为上述 HTTP 处理程序使用 ASPX 页面名称来路由资源的请求到处理程序。因此,它被视为任何其他普通页面请求,并且 ASP.NET 不返回资源,而是将请求重定向到登录页面。为了防止这种情况并允许从登录页面匿名访问资源服务器处理程序,应将以下部分添加到 Web.config 文件的 <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. -->
<location path="ResServerTest.Web.Controls.aspx">
<system.web>
<authorization>
<allow users="*"/>
</authorization>
</system.web>
</location>
这允许所有用户在无需身份验证的情况下访问资源,从而允许使用资源服务器处理程序的程序集中的类和控件在登录 Web 窗体上使用。只需修改 location
标记的 path
属性中的页面名称,使其与 HTTP 处理程序部分中使用的名称匹配。
常见错误和问题
使用资源服务器处理程序时最常见的错误是拼写错误资源名称,以及忘记将 Build Action 属性更改为 Embedded Resource。在这两种情况下,脚本和 XML 资源都会返回错误“Value cannot be null. Parameter name: stream
”。图像资源只会显示为空白。演示项目包含这些错误的示例。
另一个常见错误是在 Web.Config 文件或 Web 窗体中引用 ASPX 页面名称时拼写错误,以检索动态内容。在这种情况下,您将遇到损坏的链接或“未找到资源”的错误。如果您在 Web.Config 文件中拼写错误类名或程序集名称,应用程序将无法启动,并且会收到一条错误消息,告知您找不到指定的类型或程序集。错误消息中显示的类型或程序集将是资源处理程序类或其程序集的错误名称。更正这些名称将解决错误。
开发过程中可能出现的一个问题是修改资源后,在测试控件时看不到这些更改。原因是嵌入式资源不创建生成依赖关系。因此,修改它们不会导致程序集重新生成。在进行此类更改时,您只需要记住始终强制重新生成程序集,以便它嵌入更新的资源。您可能还需要强制刷新页面才能使其下载更新的资源(例如,在 Internet Explorer 中按 Ctrl+F5)。
您可以通过在浏览器中输入资源的 URL 来测试资源的检索并查看返回的内容。对于文本资源,在 URL 前加上“view-source:”。例如
view-source:https:///ResSrvTest/ResServerTest.Web.
Controls.aspx?Res=DemoCustomControl.js
这将检索 DemoCustomControl.js 文件并在记事本窗口中显示它。
演示
要试用演示应用程序和自定义控件,请在 IIS 中创建一个名为 ResSrvTest 的虚拟目录,并将其指向 DotNet\Web\ReServerTest\ResSrvTest 文件夹。启动页是 WebForm1.aspx。演示项目设置为在已安装 Visual Studio .NET 2003 和 IIS 的开发计算机上进行编译和运行。如果您使用的是远程服务器,则需要设置虚拟目录,单独构建演示自定义控件,然后将演示自定义控件和演示应用程序文件复制到服务器位置。
结论
我在自定义控件中使用了这种方法,这些控件利用客户端脚本和图像文件,并且发现它非常有用。涉及客户端脚本的控件开发变得更容易,控件程序集的部署也变得更容易。
修订历史
- 04/02/2006
- 移除了
ReadBinaryResource
,因为它不再需要。还添加了关于 ASP.NET 2.0 中对 Web 资源的新支持的说明。
- 移除了
- 06/27/2004
进行了一些代码调整。根据 Adam Pawsey 的建议进行了一些修改:
- 添加了一些静态辅助方法来创建资源链接。
- 添加了对加载除包含资源处理程序类的程序集之外的其他程序集中的资源的支持。
- 07/19/2003
- 添加了一个关于如何使资源服务器在应用程序中使用基于表单的身份验证时从登录 Web 窗体工作的章节。更新了 XML 代码文档。使用 Visual Studio .NET 2003 重新构建了项目。
- 04/09/2003
- 将资源服务器类更改为实现
System.Web.IHttpHandler
而不是派生自System.Web.UI.Page
。这提高了性能并进一步简化了类的使用。
- 将资源服务器类更改为实现
- 04/06/2003
- 初始发布。