从嵌入式资源加载 WebForms 和 UserControls






4.89/5 (21投票s)
从嵌入式资源加载 WebForms 和 UserControls。
引言
你是否曾经想过将那个庞大的 Web 项目分解成多个项目,或者在多个 Web 项目中重用用户控件和 Web Forms?
目前,在多个 ASP.NET 项目中重用 Web Forms 和用户控件需要复制相关的 aspx 和 ascx 文件。你可以将 Web 控件放在单独的程序集中,但你会失去设计时拖放的功能,而这正是用户控件和 Web Forms 最初易于创建的原因。如果你曾经尝试将用户控件或 Web Forms 放入单独的程序集中,你可能会遇到空白页面或运行时错误。原因在于 LoadControl()
实际上会读取 ascx 或 aspx 文件来填充 Controls
集合,然后将它们绑定到你的类变量。如果找不到 ascx 文件,除非你在代码中进行了(如 WebControls 所做的那样)添加,否则不会添加任何控件。
我想要的是能够从另一个程序集中动态调用 LoadControl()
,以便在多个 Web 项目中重用用户控件,而无需复制大量的 ascx 文件。要求太多了吗?
是否可以将这些 ascx 和 aspx 文件嵌入到程序集资源中,然后加载它们?LoadControl()
期望一个虚拟路径,并且似乎没有办法从资源流加载控件。然后我发现了这个
解决方案
ASP.NET 2.0 中的虚拟路径提供程序可以用来从你选择的位置加载 ascx 文件。对我而言,我决定将 ascx 文件存储在程序集本身中。不再有与更新的程序集不兼容的过时的 ascx 文件。只需部署一个文件,即程序集本身,如果你将该程序集添加为引用,VS 将会自动复制它!要将 ascx/aspx 文件嵌入到程序集中,你必须在属性页中将文件的“生成操作”更改为“嵌入式资源”,我们创建的虚拟路径提供程序将完成其余的工作。
当需要解析虚拟路径时,ASP.NET 会询问最后一个注册的虚拟路径提供程序文件是否存在,如果存在,它将调用 GetFile
来获取 VirtualFile
实例。
在我们加载资源之前,我们需要知道资源位于哪个程序集以及要加载的资源名称。我选择将此信息编码到虚拟路径中。我的最终 URL 看起来像这样
~/App_Resources/WebApplicationControls.dll/WebApplicationControls.WebUserControl1.ascx
它有点长,但包含了我需要的所有信息。我不想拦截所有 URL,所以我们需要能够识别哪些 URL 需要处理,哪些 URL 交给默认的虚拟路径提供程序处理。为此,我选择只处理位于 App_Resources 下的 URL。这个文件夹不存在,这就是重点,因为这个位置的所有路径都将被拦截。第二部分包含程序集名称,最后一部分是资源名称,其中包含命名空间。
我已按如下方式实现了虚拟提供程序
public class AssemblyResourceProvider : System.Web.Hosting.VirtualPathProvider
{
public AssemblyResourceProvider() {}
private bool IsAppResourcePath(string virtualPath)
{
String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return checkPath.StartsWith("~/App_Resource/",
StringComparison.InvariantCultureIgnoreCase);
}
public override bool FileExists(string virtualPath)
{
return (IsAppResourcePath(virtualPath) ||
base.FileExists(virtualPath));
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsAppResourcePath(virtualPath))
return new AssemblyResourceVirtualFile(virtualPath);
else
return base.GetFile(virtualPath);
}
public override System.Web.Caching.CacheDependency
GetCacheDependency( string virtualPath,
System.Collections.IEnumerable virtualPathDependencies,
DateTime utcStart)
{
if (IsAppResourcePath(virtualPath))
return null;
else
return base.GetCacheDependency(virtualPath,
virtualPathDependencies, utcStart);
}
}
IsAppResource
是一个私有的辅助方法,用于确定我们是否应该处理请求,还是让默认提供程序处理请求。虚拟路径提供程序是链式连接的,因此调用基类很重要。还有一个必要的操作是重写 GetCacheDependency
以返回 null
,否则 ASP.NET 将尝试监视文件更改并引发 FileNotFound
异常。请注意,GetFile
返回一个 AssemblyResourceVirtualFile
的实例,此类提供一个 Open()
方法来获取资源流,实现如下
class AssemblyResourceVirtualFile : VirtualFile
{
string path;
public AssemblyResourceVirtualFile(string virtualPath) : base(virtualPath)
{
path = VirtualPathUtility.ToAppRelative(virtualPath);
}
public override System.IO.Stream Open()
{
string[] parts = path.Split('/');
string assemblyName = parts[2];
string resourceName = parts[3];
assemblyName = Path.Combine(HttpRuntime.BinDirectory,
assemblyName);
System.Reflection.Assembly assembly =
System.Reflection.Assembly.LoadFile(assemblyName);
if (assembly != null)
{
return assembly.GetManifestResourceStream(resourceName);
}
return null;
}
}
我们在这里需要做的就是解析 virtualPath
来获取程序集名称和资源名称。我假设程序集位于 bin 目录中,因为这是它们的典型位置,并且 GetManifestResourceStream
会完成所有繁重的工作。
现在我们只需要告诉 ASP.NET 使用我们的虚拟路径提供程序。这可以在两个地方之一完成:Global.asax 中的 Application_Start()
,或者 App_Code 目录中的任何类文件中的签名如下的静态方法
public static void AppInitialize() { ... }
重要的是,它必须在我们提供程序捕获的任何虚拟路径被解析之前注册。我选择将其放在 Application_Start 中,如下所示。
protected void Application_Start(object sender, EventArgs e)
{
System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(
new AssemblyResourceProvider());
}
现在,我们可以使用常规的 LoadControl
将我们的自定义控件加载到 PlaceHolder
中,如下所示
protected void Page_Load(object sender, EventArgs e)
{
Control ctrl = LoadControl("/App_Resource/WebApplicationControls.dll/"+
"WebApplicationControls.WebUserControl1.ascx");
PlaceHolder1.Controls.Add(ctrl);
}
URL 有点长,因为它们包含程序集和类名称的信息,但你可以拦截任何你想要的 URL。我选择将其前缀为 App_Resources,以便更容易识别要拦截的 URL。另一种方法是定义一个虚拟路径提供程序,该提供程序使用反射搜索 bin 目录中的所有程序集,查找以 ascx/aspx 结尾的资源,并只拦截带有或不带有命名空间的 URL。
Using the Code
演示项目使用的是 VS 2005 的 Web 应用程序项目更新,可以在这里找到:MSDN。