扩展 ASP.NET 2.0 资源提供程序模型
原文:http://www.microsoft.com/china/msdn/library/webservices/asp.net/ExASPNET20RPM.mspx
本页内容
简介 | |
哎哟,我的资源应去哪里? | |
资源提供程序模型 | |
构建数据库资源提供程序 | |
从外部程序集访问资源 | |
支持自定义本地化表达式 | |
结束语 | |
致谢 | |
其他资源 |
简介
ASP.NET 2.0 在本地化 Web 应用程序方面进行了许多奇妙的改进。我曾在 MSDN 文章“ASP.NET 2.0 Localization Features: A Fresh Approach to Localizing Web Applications(英文)”中介绍了这些新功能。
熟悉了这些新的本地化功能后,您将立即注意到以下方面:
• |
现在通过 Visual Studio 2005 可以轻松地生成各页面的资源,在页面设计视图中调用“Generate Local Resource”(生成本地资源)菜单项即可。 |
• |
借助于更好的资源编辑器和强类型化访问,创建和使用全局资源要简单得多。 |
• |
使用声明性本地化表达式,可以很轻松地将资源条目映射到控件属性和内容区。 |
• |
ResourceManager 不再需要手动安装,因为 ResXResourceProviderFactory 协同从本地或全局资源检索资源条目,根据需要分配 ResourceManager。 |
• |
自动检测浏览器区域首选项并将该区域分配给请求线程,从而更易于尊重用户(甚至匿名用户)的区域首选项。 |
即使有了所有这些奇妙的优点,我们往往还想要更多,这并不奇怪。使用这些重大功能本地化站点后不久,您可能开始考虑一些其他事了,例如:
• |
如何从备选位置(如单独的资源程序集或数据库源)提取资源? |
• |
如何管理既使用一些本地和全局资源又有替代数据源的混合环境? |
• |
如何能控制资源的来源并继续利用 ASP.NET 2.0 资源提供程序模型、本地化表达式及其他设计器集成功能? |
• |
如何利用现有的本地化功能和可用的扩展性选项,以更好地满足我的开发环境及本地化过程的需要? |
这就是可扩展性为何如此重要的原因。有多种方式可扩展 ASP.NET 本地化功能并与开发环境交互。本文是三部分系列文章的第一篇,将帮助您应用 ASP.NET 的扩展功能处理企业本地化方案并改进本地化开发过程。
在本文中,我将重点介绍此类功能,使您能够从备选存储位置检索资源,并与页面解析、编译和运行时执行集成。我将说明如何组合使用自定义资源提供程序、自定义表达式构建器及其他支持的可扩展类型来实现这一目的。本系列文章的第二篇将显示如何通过将所选的资源存储与 Visual Studio 2005 中的内置高效功能集成,以进一步改进开发过程。第三篇文章将给出处理复杂资源层次结构(例如,可支持客户端定制)的备选方案。
哎哟,我的资源应去哪里?
将本地化的资源合并到 Web 站点往往是一件痛苦的工作。生成资源通常很难,组织翻译资源往往需要托管进程,但 Web 站点中资源的更为困难方面是知道资源中应包括哪些内容、如何分配这些资源及哪些因素将影响最佳性能和维护性。
使用 ASP.NET 2.0 创建和访问资源
ASP.NET 2.0 提供了生成各页面本地资源的功能。因此,出现了更有效的页面设计和国际化过程。
通过应用静态 HTML 和 ASP.NET 服务器控件组合设计页面。
1. |
通过将静态区与 ASP.NET 本地化控件一起封装,准备好用于本地化的静态区。 |
2. |
向所有服务器控件提供适当控件名称,以便可以很容易地识别生成的事件处理程序及资源键。 |
3. |
在 App_GlobalResources 子目录中创建共享资源。这些可以是已经存在的 .resx 文件,也可以是创建的用于保存多个页面间共享项的新 .resx 文件。 |
4. |
在适当的时候使用显式资源表达式将共享资源与控件属性关联。最好在生成页面的本地资源前执行此操作。 |
5. |
通过选择“Generate Local Resource”(生成本地资源)菜单项,在页面设计视图中生成本地资源。 |
生成本地资源后,页及其控件的所有本地化属性将被传送到单独的本地资源文件,每页一个。隐式本地化表达式告诉页解析程序基于公用前缀生成代码,这些代码将控件的各资源值映射到其相应属性。请考虑示例代码中 Expressions.aspx 页的以下隐式表达式。
<asp:Label ID="labHelloLocal" runat="server" Text="Hello" meta:resourcekey="labHelloLocalResource1" ></asp:Label>
资源存储在 App_LocalResources 目录下的 Expressions.aspx.resx 文件中。此 Label 控件的资源共享前缀“labHelloLocalResource1”;例如,Text 属性由“labHelloLocalResource1.Text”键存储。
如果您很好地分解用户界面,使用主页和用户控件用于公共用户界面区,则为各主页、用户控件和页生成的最终资源也将在一定程度上得到合适的分解(重叠减少)。这使得组织各页面部分使用的资源更容易,这在过去的版本中通常是很麻烦的事。有时您还要从共享位置提取资源。此种情况下,您将提供一个显式资源表达式,如此处显示的 $Resources 表达式。
<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>"></asp:Label>
此种情况下,资源位于 App_GlobalResources 目录下的 CommonTerms.resx 中。可使用“表达式编辑器”(请参见先前提到的 MSDN 文章)创建此类显式表达式,以简化过程。
隐式表达式和显式表达式都触发代码生成以使用资源提供程序检索资源值。这些声明性表达式,结合代码和资源生成,提供了一个前所未有的高效工具,至少对 Web 应用程序是这样。
资源程序集和 ResourceManager
有几种方法可编译及部署 ASP.NET 2.0 应用程序:
• |
部署源代码并且 JIT 编译整个站点。 |
• |
使用可更新的页和资源重新编译站点。 |
• |
重新编译站点以每个页生成一个程序集或每个目录生成一个程序集。 |
无论哪种情况,最后都将为站点中的各目录创建资源程序集,并在其各自的语言特定目录下生成附属程序集。即使站点是 JIT 编译的,结果也是一样。图 1 说明了带有两个子目录的站点以及单独转换为西班牙语的预编译结果。
图 1. 为 ASP.NET Web 站点中各目录生成的资源及附属程序集
这些资源在运行时通过 ResourceManager 访问。请求资源时,为各资源类型(例如,Page1.aspx 和 Page2.aspx)分配一个 ResourceManager。与各资源类型关联的资源程序集在第一次访问时加载到 ASP.NET 应用程序域,并保留在域中,直到应用程序域卸载为止。在图 1 的情况中,第一次针对西班牙区域访问 \SubDir1\Page1.aspx 时,程序集 \es\App_LocalResources.subdir1.cdcab7d2.resources.dll 加载到应用程序域中。此程序集包含 \SubDir1 中所有页的西班牙语资源。
图 2 说明 ResourceManager 如何访问特定页的本地资源。加载 \SubDir1\Page1.aspx 时,由隐式表达式生成的代码将调用 ResXResourceProviderFactory,其返回 LocalResXResourceProvider。此提供程序在 App_LocalResources.subdir1.cdcab7d2 程序集中为 Page1.aspx 类型创建一个 ResourceManager。如果请求线程具有“es”的 UI 区域,则将 \es 目录中的附属资源程序集加载到应用程序域。如果 UI 区域没有匹配的附属程序集,则 ResourceManager“退回”到主资源程序集。
图 2. 加载到应用程序域后,ResourceManager 访问主资源程序集或本地化的附属程序集中的资源
资源和附属程序集在应用程序域中保持加载状态。各 ResourceManager(按页或共享资源类型)也被缓存,并重用于对与其关联资源的后续请求。
我刚才描述的行为概述了如何使用默认 ASP.NET 资源提供程序模型访问资源。接下来讨论您可能改变此默认实现的原因。
为什么使用一个备选位置?
对于新的 ASP.NET 2.0 体验,如果您采用资源生成和运行时访问的默认值,它将产生较之过去更良好的体验。尽管如此,它仍经常需要使用备选的资源存储,原因如下:
• |
重新使用已经位于备选存储的现有资源 |
• |
存储更大静态内容块的实用性 |
• |
易管理性 |
资源存储的两个常用备选位置是外部资源程序集和数据库。
处理预存在资源 — 您可能有来自较早应用程序的预存在资源程序集或在 Windows 和 Web 应用程序间共享的资源程序集。通常,如果您要将代码从 ASP.NET 1.1 迁移到 2.0,建议您采用 1.1 应用程序中的 .resx 文件,并将其复制到 2.0 应用程序的 App_GlobalResources 目录。然后,这些 .resx 将使用 ASP.NET 2.0 应用程序进行编译并可通过强类型全局资源进行访问。但是,要对预存在资源程序集进行版本控制,或仅维护用于 Windows 和 Web 应用程序的资源的一个副本,此方法不适用。因此,将这些资源存储在仅用于共享资源的程序集中是一个更好的办法。这意味着需要一种方法将资源从这些程序集中提取出来。
在数据库中存储资源 — 出于许多原因,数据库存储是针对 Web 应用程序资源的一种常见方法。您可能已经猜到了,有些时候对于具有成千上万个页面和数千个资源条目的站点而言,使用程序集资源可能不理想。它将增加运行时内存使用,而且还会增加加载到应用程序域的程序集数。这两种结果都可能对超大型站点的性能产生负面影响,而使用数据库,只是增加了调用的等待时间,相对而言,更为值得。数据库资源还可能为本地化过程、减少重复、复杂的缓存选项及存储可能的更大内容块提供了更为灵活和可管理的环境。最后,将资源分配给数据库可支持更复杂的转换内容的层次结构,其中客户或部门可能已经有了文本的自定义版本,这些文本以后也将本地化。
ASP.NET 本地化的扩展模式特别设计以支持资源存储的备选位置,从而可很容易地实现这些结果。此外,您还可以挂接到设计时体验,这样开发人员不仅可从备选存储检索资源,也可以在备选存储中生成资源,在接下来的文章中对后一主题进行了讨论。
无论是在外部资源程序集还是在数据库中存储资源,您一定想利用 ASP.NET 2.0 的本地化功能。目的是从任一位置访问资源的同时,继续使用本地化表达式和本地化 API。使用扩展功能可实现这一目的,在本文的剩余部分,我将讨论这些功能。
资源提供程序模型
如前所述,ResourceManager 类型负责在运行时从程序集检索资源。它根据请求线程的当前 UI 区域(图 2),封装正确资源集的检索。换言之,只要请求线程设置为正确的 UI 区域以调用用户的首选项,ResourceManager 就具有所有逻辑以处理资源回退,并从正确的附属程序集中选择正确的资源。在 ASP.NET 2.0 之前,我们必须编写自己的代码以实例化各资源类型的 ResourceManager,并管理其生存期。这需要适合各页面请求的额外代码以创建或访问 ResourceManager 实例并调用方法以访问资源条目。要以声明方式将资源绑定到页元素,可以使用自定义的数据绑定语句,但这同样需要代码以启动页面级数据绑定和绑定变量分配。
在 ASP.NET 2.0 中,可以从任何页面使用本地化 API 函数或使用控件来检索资源。例如,以下代码分别检索本地页面资源和全局资源。
this.labHelloLocal.Text = this.GetLocalResourceObject("labHelloLocalResource1.Text") as string; this.labHelloGlobal.Text = this.GetGlobalResourceObject("CommonTerms", "Hello") as string;
我前面提到,声明性本地化表达式也可用于设置资源中的页和控件属性。隐式本地化表达式,如:
<asp:Label ID="labHelloLocal" runat="server" Text="Hello" meta:resourcekey="labHelloLocalResource1" ></asp:Label>
和显式本地化表达式,如:
<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>" ></asp:Label>
用于生成代码以调用 GetLocalResourceObject() 和 GetGlobalResourceObject()。应该告诉您的是,ASP.NET 2.0 访问资源的方式最终是通过这些方法实现的,即使您利用了声明性表达式的便利。
这也正是资源提供程序模型起作用的地方。这些 API 调用依赖默认或自定义 ResourceProviderFactory 才能找到正确的资源条目并收集其值。默认的 ResourceProviderFactory 为上面提到的 ResXResourceProviderFactory 类型。此工厂为全局资源返回 GlobalResXResourceProvider 的一个实例,为本地页面资源返回 LocalResXResourceProvider 的一个实例。
最后,这些提供程序依赖 ResourceManager 以访问相应附属程序集中的各个资源。提供程序使用 ResourceReader 在页面解析步骤期间收集页面资源集合。图 3 说明了与默认资源提供程序模型关联的这些重要组件。
图 3 组成默认资源提供程序模型的组件:提供程序工厂、本地和全局资源提供程序、资源管理器及访问各资源类型的资源读取器
此提供程序具有以下一些优点:
1. |
它处理各 ResourceManager 的激活和生存期。 |
2. |
本地化表达式及其他资源 API 利用提供程序查找资源,因此通过简化和提取的 API 增加了生产率。 |
3. |
提供程序模型可扩展,因此可以继续利用 ASP.NET 2.0 的生产力功能的同时,更改资源存储位置。 |
现在,我将介绍如何建立自定义资源提供程序。
构建数据库资源提供程序
利用自定义资源提供程序,您可以访问不是来源于 App_GlobalResources 或 App_LocalResources 中的资源。例如,可以使用自定义资源提供程序来访问部署在预编译程序集中的资源,或访问数据库中的内容。在本节中将讨论数据库资源提供程序模型,将在本文的稍后部分对访问外部资源程序集做以探讨。
自定义资源提供程序包含一个 ResourceProviderFactory 和至少一个实现 IResourceProvider 接口的资源提供程序类型。工厂负责将合适的 IResourceProvider 实例化以访问本地或全局资源。图 4 说明了在本文的示例代码中构成数据库资源提供程序模型实现的组件。
图 4. 自定义数据库资源提供程序模型的组件层次结构
数据库资源条目
先来回顾一下要存储实际资源条目的数据库表结构或许对您有所帮助。示例包含一段 SQL 脚本,用于创建名为 CustomResourceProvidersSample 的数据库,还包含一个名为 StringResources 的表。表 1 包含以下字段:
表 1. 具有资源条目的数据库表 | |
字段 | 说明 |
resourceType |
每种资源的类别。可用其区分不同页面的本地资源,或根据用户定义的名称区分全局资源类型。 |
cultureCode |
来自 .NET 所使用的受支持的 CultureInfo 代码中的区域性代码,基于 ISO 标准。还可以针对任何缺失代码来扩展该代码。 |
resourceKey |
用于检索资源的资源键。 |
resourceValue |
资源值。此表支持字符串多达 4K。 |
在本示例中,所有资源均存储在一个单独的表中,尽管在更复杂或更大规模的环境下为了优化典型的使用模式,可以将其分布于几个表之中。此表的主键是一个组合键,包括 resourceType、cultureCode 和 resourceKey。单个资源值通常使用主键请求。图 5 显示了表内容的部分视图。
图 5. 此示例资源条目的部分视图
页面资源的 resourceType 是页面名称,包括其在应用程序中的相对路径(即 Expressions.aspx,SubDir1/Expressions.aspx)。此惯例会区分不同子目录中相同名称的页面,这与默认资源提供程序模型按子目录来识别不同的本地资源程序集的方式类似。控件属性的资源键遵循与典型的页面资源相同的命名惯例,即使用控件前缀和属性名称,语法如下。
[Prefix].[PropertyName]
全局资源具有用户定义的 resourceType。示例代码具有几种全局资源类别:Glossary、CommonTerms 和 Config。本例中的资源键很直观针对其内容进行命名。
数据访问层 StringResourcesDALC 将根据提供程序模型的使用模式来提取工作以从该表中检索资源。
扩展 ResourceProviderFactory
ResourceProviderFactory 类型是 ASP.NET 2.0 中资源访问的中心,负责根据请求的资源类型来返回全局或本地资源提供程序。ResourceProviderFactory 是一个抽象的基类型,需要以下两种方法的实现:CreateLocalResourceProvider() 和 CreateGlobalResourceProvider()。要创建自定义提供程序工厂,则需继承此基类型来提供这些方法的实现。两种方法必须返回实现 IResourceProvider 接口的资源提供程序的实例。
在代码清单 1 中显示了基本的 ResourceProviderFactory 类型声明。
代码清单 1. ResourceProviderFactory 抽象类型
public abstract class ResourceProviderFactory { protected ResourceProviderFactory(); public abstract IResourceProvider CreateGlobalResourceProvider(string classKey); public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath); }
ResourceProviderFactory 在编译时向页面解析步提供资源提供程序,在运行时则为本地化 API 调用提供资源提供程序。
• |
页面解析器 — 在设计时解析页面并将其作为页面编译的预指针。在此过程中本地和全局资源的显式表达式有效。在编译期间,在编译页面中生成所有表达式的代码。在此过程中分析器使用资源提供程序。 |
• |
运行时 — 在运行时,表达式不再具有编译页面中的含义。在编译期间生成的代码使用本地化 API 访问本地和全局资源。资源提供程序为本地和全局资源类型而创建。 |
在示例代码中,DBResourceProviderFactory 为两个路径均创建了 DBResourceProvider。这是因为本地和全局资源通过同一方式访问。DBResourceProviderFactory 的代码显示在代码清单 2 中。
代码清单 2. DBResourceProviderFactory 是支持数据库资源的 ResourceProviderFactory 的自定义实现。
using System; using System.Web.Compilation; using System.Web; using System.Globalization; namespace CustomResourceProviders { public class DBResourceProviderFactory : ResourceProviderFactory { public override IResourceProvider CreateGlobalResourceProvider (string classKey) { return new DBResourceProvider(classKey); } public override IResourceProvider CreateLocalResourceProvider (string virtualPath) { string classKey = virtualPath; if (!string.IsNullOrEmpty(virtualPath)) { virtualPath = virtualPath.Remove(0, 1); classKey = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); } return new DBResourceProvider(classKey); } } }
对于隐式表达式或调用本地资源的显式表达式,将调用 GetLocalResourceProvider() 来创建页面的提供程序。以下是隐式表达式和使用本地资源的显式表达式的示例 — 已在示例代码的 Expressions.aspx 页面中定义。
<asp:Label ID="labHelloLocal" runat="server" Text="HelloDefault" meta:resourcekey="labHelloLocalResource1" ></asp:Label> <asp:Label ID="Label1" runat="server" Text="<%$ Resources:labHelloLocalResource1.Text %>" ></asp:Label>
GetLocalResourceProvider() 使用一个单独的参数,该参数为包含应用程序目录的页面的虚拟路径。上面的两个表达式将传递“/LocalizedWebSite/Expressions.aspx”给此参数。从图 5 中,您可以看出本地资源使用代表页面相对路径的 resourceType 进行存储,其中不包含应用程序目录。这样,在创建 DBResourceProvider 示例之前,GetLocalResourceProvider() 会将应用程序目录从路径中去掉。
对于请求全局资源的显式表达式,会将在表达式中直接指定的资源类型传递至 GetGlobalResourceProvider()。请注意下面的显式表达式(同样来自代码示例中的 Expressions.aspx 页面)。
<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>"></asp:Label>
本例中的资源类型为 CommonTerms,因此调用 GetGlobalResourceProvider() 来将 CommonTerms 作为参数传递。为此类型创建了一个 DBResourceProvider 实例。
对于任何给定的资源类型,仅创建一个 DBResourceProvider 实例。实例创建之后会被缓存以备将来之用。因此,只有缓存中尚不存在提供程序实例时才会调用工厂。创建和缓存提供程序的过程封装在用于访问资源的本地化 API 中。
ResourceProviderFactory 配置
运行时将使用 ResxResourceProviderFactory,除非在配置中指定一个替代的 ResourceProviderFactory 类型。Web 配置文件的 <globalization> 部分有一个名为 resourceProviderFactoryType 的属性。此处,您指定了应使用 ResourceProviderFactory 类型。要配置 DBResourceProviderFactory,您要添加以下设置。
<system.web> ...other settings <globalization uiCulture="auto" culture="auto" resourceProviderFactoryType="CustomResourceProviders.DBResourceProviderFactory, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1" /> </system.web>
注意 在所提供的示例代码中,DBResourceProviderFactory 归属于 CustomResourceProviders 程序集中的 CustomResourceProviders 命名空间。可以强命名程序集并将其安装到全局程序集缓存 (GAC) 中。
现在,将使用 DBResourceProviderFactory 创建页面解析期间和运行时的资源提供程序。
实现 IResourceProvider
资源提供程序模型的核心是资源提供程序类型。尽管 ResourceProviderFactory 是一个很重要的抽象形式,但资源提供程序最终还是要负责在运行时返回资源条目(无论它们存储在何处)。正如在前一节中所述,提供程序由 ResourceProviderFactory 实现创建,然后进行缓存以备今后之用。从图 4 所示的数据库资源提供程序模型中,您可以看出 DBResourceProvider 类型用于本地和全局资源皆可。此类型负责从数据库检索资源,但是它使用 DBResourceReader 和 StringResourcesDALC 组件来处理该任务。
资源提供程序实现了 IResourceProvider 接口,如代码清单 3 所示。
代码清单 3. IResourceProvider 接口
public interface IResourceProvider { object GetObject(string resourceKey, CultureInfo culture); IResourceReader ResourceReader { get; } }
各资源通过 GetObject() 进行检索,且 ResourceReader 属性应根据提供程序实例的资源类型返回资源的集合。
在页面解析步期间,提供程序用于检索页面的全部本地资源;显式表达式有效;还有,在编译期间,还会生成页面的代码。对于本地资源,资源读取器用于为隐式表达式生成代码。通过在适当的提供程序上对 GetObject() 进行调用,本地和全局资源的显式表达式分别有效。
在运行时,解析器生成的代码会触发对 GetObject() 的调用以在初始化页面时检索本地和全局资源。
检索单独的数据库资源
以下为针对 GetObject() 的 DBResourceProvider 实现。
public object GetObject(string resourceKey, CultureInfo culture) { if (string.IsNullOrEmpty(resourceKey)) { throw new ArgumentNullException("resourceKey"); } if (culture == null) { culture = CultureInfo.CurrentUICulture; } string resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey); }
实际上,可将检索资源的工作委派给 StringResourcesDALC 类型(请参见图 4)以处理数据库查询。此组件将提供程序与资源回退及其他需要查找实际资源的逻辑相隔离。
GetResourceByCultureAndKey() 初始化数据库连接并执行 SqlDataReader 以检索值,包括必需的资源回退逻辑(稍后再加以讨论)。
在批处理中检索资源
DBResourceProvider 在以下 ResourceReader 属性的实现中返回一个 DBResourceReader 实例。
public System.Resources.IResourceReader ResourceReader { get { ListDictionary resourceDictionary = this.m_dalc.GetResourcesByCulture(CultureInfo.InvariantCulture); return new DBResourceReader(resourceDictionary); } }
StringResourcesDALC 负责收集特定类型 (InvariantCulture) 的默认资源。为了实现枚举,由查询结果创建的 ListDictionary 被 DBResourceReader 所包装。
DBResourceReader 实现 IResourceReader。此实现的关键元素如下所示。
public class DBResourceReader : DisposableBaseType, IResourceReader, IEnumerable<KeyValuePair<string, object>> { private ListDictionary m_resourceDictionary; public DBResourceReader(ListDictionary resourceDictionary) { this.m_resourceDictionary = resourceDictionary; } public IDictionaryEnumerator GetEnumerator() { return this.m_resourceDictionary.GetEnumerator(); } // 其他方法 }
页面解析器使用读取器的字典枚举器为隐式表达式生成代码。如果未提供读取器或读取器中字典为空,则代码无法生成。隐式表达式不必为每一个属性值均显示值,因为它不是显式的。因此,对于隐式表达式,如果未生成代码来设置值,则默认属性值会随页面一同呈现。
资源回退
资源回退是资源提供程序实现的一个重要部分。资源根据请求线程的当前 UI 区域在运行时被请求。
System.Threading.Thread.Current.CurrentUICulture
如果该区域为一特定区域(如“es-EC”或“es-ES”),则资源提供程序应查看是否存在此特定区域的资源。不过,有可能仅指定了针对中立区域(如“es”)的资源。中立区域为父项。如果未发现任何特定条目,则接下来检查父项。最终应使用应用程序的默认区域,以便能找到值。在此示例中,默认区域为“en”。
资源回退由数据访问组件 StringResourcesDALC 来封装。在使用调用来检索资源时,GetResourceByCultureAndKey() 会被调用。此函数负责打开数据库连接,调用将执行资源回退的递归函数,随后关闭数据库连接。下面显示了 GetResourceByCultureAndKey() 的实现。
public string GetResourceByCultureAndKey(CultureInfo culture, string resourceKey) { string resourceValue = string.Empty; try { if (culture == null || culture.Name.Length == 0) { culture = new CultureInfo(this.m_defaultResourceCulture); } this.m_connection.Open(); resourceValue = this.GetResourceByCultureAndKeyInternal (culture, resourceKey); } finally { this.m_connection.Close(); } return resourceValue; }
递归函数 GetResourceByCultureAndKeyInternal() 首先尝试查找特定区域的资源。如果没有找到,则搜索父项区域然后重试查询。如果仍未成功,则最后再尝试使用默认区域来查找资源条目。如果使用默认区域仍没有找到资源条目,则可能是示例中发生了严重异常。下面显示了 GetResourceByCultureAndKeyInternal() 的代码清单。
private string GetResourceByCultureAndKeyInternal (CultureInfo culture, string resourceKey) { StringCollection resources = new StringCollection(); string resourceValue = null; this.m_cmdGetResourceByCultureAndKey.Parameters["cultureCode"].Value = culture.Name; this.m_cmdGetResourceByCultureAndKey.Parameters["resourceKey"].Value = resourceKey; using (SqlDataReader reader = this.m_cmdGetResourceByCultureAndKey.ExecuteReader()) { while (reader.Read()) { resources.Add(reader.GetString(reader.GetOrdinal("resourceValue"))); } } if (resources.Count == 0) { if (culture.Name == this.m_defaultResourceCulture) { throw new InvalidOperationException(String.Format( Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_DefaultResourceNotFound, resourceKey)); } culture = culture.Parent; if (culture.Name.Length == 0) { culture = new CultureInfo(this.m_defaultResourceCulture); } resourceValue = this.GetResourceByCultureAndKeyInternal(culture, resourceKey); } else if (resources.Count == 1) { resourceValue = resources[0]; } else { throw new DataException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_DuplicateResourceFound, resourceKey)); } return resourceValue; }
作为一种选择,资源回退也可以封装在存储过程或 SQL CLR 组件之中 — 因为回退的规则在某种程度上可能与数据库设计相结合,因此其对于业务层而言就不是特别重要。
资源缓存
采用默认的提供程序模型,在从资源程序集中提取资源时,会加载一次程序集并将其缓存在应用程序域中。对于数据库资源,我们必须实现自己的缓存机制以避免每次单个请求资源都要针对数据库。DBResourceProvider 会处理此任务。
先前,我讲述了在没有缓存的情况下提供程序的 GetObject() 实现的情形。下列代码为使用一个对数据访问层的调用从数据库中检索资源。
resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);
请记住,每个资源类型均存在一个单独的提供程序实例,并且该实例已被缓存以供反复使用。在提供程序内部,如果我们在字典中为每一个请求的区域缓存资源条目,则这些字典条目会通过提供程序缓存到内存之中。检索对象的代码可能首先针对此值查找字典缓存,如果没有找到,则在从数据库中检索该值之后创建一个缓存条目。结果显示如下。
string resourceValue = null; Dictionary<string, string> resCacheByCulture = null; if (m_resourceCache.ContainsKey(culture.Name)) { resCacheByCulture = m_resourceCache[culture.Name]; if (resCacheByCulture.ContainsKey(resourceKey)) { resourceValue = resCacheByCulture[resourceKey]; } } if (resourceValue == null) { resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey); lock(this) { if (resCacheByCulture == null) { resCacheByCulture = new Dictionary<string, string>(); m_resourceCache.Add(culture.Name, resCacheByCulture); } resCacheByCulture.Add(resourceKey, resourceValue); } } return resourceValue;
缓存是在数据库中存储资源时保证执行性能的一个必要环节。在本例中,这些值将被缓存直至应用程序域被释放为止,这表明对数据库中资源的动态更新在运行时不会受到影响,除非您重启应用程序。为允许此类型的动态更新,必须进行缓存资源(这些资源与数据库缓存有依赖关系)的其他工作。
线程安全
在 Web 环境下我们考虑的另一件事情是线程安全。使用 .NET 同步技术所设计的数据库提供程序模型中的组件(如图 4 所示)是线程安全的。
某特定资源类型的 DBResourceProvider 或 StringResourcesDALC 实例可以由多线程(简单的情况如同一页面的两个请求)调用。在 StringResourcesDALC 组件中,从数据库中检索数据的公共方法修改了类型的实例变量,包括打开连接、设置查询参数值和执行 SqlDataReader。为使这些功能线程安全,已应用了 MethodImplAttribute。
[MethodImpl(MethodImplOptions.Synchronized)]
该属性在方法调用期间锁定 StringResourcesDALC 对象,阻止其他调用者调用。如果缓存了资源,则不会调用数据访问组件,因此性能得到增强。
在 DBResourceProvider 中,会使用一个传统的锁定语句将对目录缓存的修改也锁定。
lock(this) { ... }
在先前部分中已显示了此缓存代码的扩展视图。锁定语句会在代码阻塞期间锁定整个对象及其成员。这表明一次仅有一个线程可以向缓存中添加值。
使用自定义资源提供程序
过去,我们经常为每一资源类型手动编写 ResourceManager 的实例化和生存期管理代码。利用 ASP.NET 2.0,资源提供程序模型会替我们处理这些工作,根据需要创建和缓存资源提供程序,只要我们针对本地化 API 编程即可。这表明我们应该使用 ASP.NET 2.0 技术来访问资源,方法如下:
• |
页面对象方法。 |
• |
HttpContext 方法。 |
• |
本地化表达式。 |
母版页、网页及用户控件均共享常见基本类型 TemplateControl。此基本类型为资源访问提供两个我先前提过的重载操作:GetLocalResourceObject() 和 GetGlobalResourceObject()。这些操作使用缓存资源提供程序通过提供程序的 GetObject() 实现来检索资源。如果还未缓存提供程序,则 ResourceProviderFactory 通常使用 CreateLocalResourceProvider() 或 CreateGlobalResourceProvider() 来创建提供程序。该方法的好处是您可以很方便地编写页面代码来检索资源值。
this.labHelloLocal.Text = this.GetLocalResourceObject("labHelloLocalResource1.Text") as string; this.labHelloGlobal.Text = this.GetGlobalResourceObject("CommonTerms", "Hello") as string;
事实上,在编译期间会根据隐式和显式表达式为每个页面生成非常相似的代码。
也可以通过 HttpContext 类型的静态方法访问本地和全局资源,这在编写不属于页面部分的代码时很有用。
this.labHelloLocal.Text = HttpContext.GetLocalResourceObject("/RuntimeCode.aspx", "labHelloLocalResource1.Text") as string; this.labHelloGlobal.Text = HttpContext.GetGlobalResourceObject("CommonTerms", "Hello") as string;
实际上,本地化表达式是一种访问资源的更便捷方式。本地化表达式提供的声明性模型可以通过本地化 API 自动生成访问资源的代码。因此,所有路径均指向本地化 API 和经过配置的 ResourceProviderFactory。
数据库资源:优点和缺点
向数据库移动资源有很多好处,包括如下几个方面:
• |
您可以为资源引入复杂的层次结构要求而不影响调用代码,例如,允许客户或部门自定义默认字符串,同时仍允许每一方转换这些字符串。 |
• |
因为可以对内容进行组织,因此可以更加轻松地在数据库层管理更大的 HTML 内容块;并且由于缓存和内存的使用,对其管理起来也更具灵活性(请记住,在默认情况下,附属资源及其所有的嵌入内容均载入应用程序域中,而使用更良好的算法则可以缓存数据库资源或将资源从缓存中释放)。 |
• |
在单独位置(数据库)而非多个 .resx 文件中存储信息可以改进本地化应用程序的整体可管理性。也可以简化使用转换器的方法。 |
数据库存储也有一些缺点:
• |
必须有附加的预见和安排。如何将资源组织到表之中?它们应该全在一个单独的表中吗?它们应该按类别划分吗?为在各表之间提供随后的分发,是应该通过单独的存储过程还是通过 SQL CLR 组件来访问资源? |
• |
为将 Visual Studio 2005 的高效功能与您的数据库资源相集成,您必须要做一番工作。就是说,您无法利用“生成本地资源”在数据库中自动生成资源,或在“表达式对话框”中查看数据库信息等等。此级别的集成需要您为开发人员构建与设计时体验相集成的自定义组件,这点我将在下文中讨论。 |
尽管需要进行将高效功能与数据库资源相集成的工作,但还是会有人认为利大于弊。并且,必须计划资源的结构和组织是我们份内之事,即使是默认的资源分配结构也是如此!
从外部程序集访问资源
还可以使用资源提供程序模型从预编译的外部程序集访问资源。这样便可在 Web 应用程序和 Windows 应用程序之间共享公用资源,同时提供一体化的版本控制和部署。在本节中,我将解释如何应用刚刚讨论的概念来访问此类型外部资源程序集。
图 6 显示了组成此外部资源提供程序模型的组件。
图 6. 外部资源提供程序模型的组件层次结构
您会注意到有关此项实现的一些内容:
• |
只支持全局资源。替换随 ASP.NET 2.0 一起免费提供的页面资源模型没有意义。从外部程序集只能得到全局资源。 |
• |
我们无法从 ExternalResourceProviderFactory 访问 LocalResXResourceProvider。这是无法从代码构建的内部类型。如果我们用 ExternalResourceProviderFactory 替换默认提供程序,则只有全局资源会受到支持(我将在以后章节讨论一种替代方法)。 |
• |
ResourceManager 用于访问资源。默认 ResourceManager 已经为我们提供了一种从程序集访问资源的方法,因此我们无需替换此项访问外部资源的功能。 |
现在,我将说明此项实现的重点。
相同的本地化表达式,不同的使用案例
请记住,资源提供程序是因本地化表达式和本地化 API 而被调用的。要访问外部资源,将使用显式表达式。这些表达式将与用于访问全局资源的表达式类似,只有少许变化;尤其是程序集名称必须与资源类型一起提供。默认提供程序知道如何找到全局资源程序集。此外部资源提供程序依赖程序集名称以获得相同的结果。
默认提供程序模型(显式全局资源)的 $Resources 表达式语法如下。
<%$ Resources: [resourceType], [resourceKey] %>
配置 ExternalResourceProviderFactory 时,可使用同一表达式访问外部资源,语法变化如下。
<%$ Resources: [assemblyName]|[resourceType], [resourceKey] %>
例如,要从 CommonResources.dll 程序集、全局资源类型“CommonTerms”检索资源,将使用以下显式表达式。
<asp:Label ID="labGlobalResource" runat="server" Text="<%$ Resources:CommonResources|CommonTerms, Hello %>" ></asp:Label>
编译页面时,将生成以下代码。
labGlobalResource.Text = this.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello");
这说明只要提供了正确的信息,外部资源提供程序模型就能通过本地化 API 利用现有表达式和代码。最后将由 ExternalResourceProvider 将信息从资源类型解析为单独的程序集名称。
ExternalResourceProviderFactory
同 DBResourceProviderFactory 一样,ExternalResourceProviderFactory 继承 ResourceProviderFactory 并能替换 CreateGlobalResourceProvider() 和 CreateLocalResourceProvider()。列表 4 显示了完整实现。
列表 4. 实现 ExternalResourceProviderFactory
public class ExternalResourceProviderFactory : ResourceProviderFactory { public override IResourceProvider CreateGlobalResourceProvider (string classKey) { return new GlobalExternalResourceProvider(classKey); } public override IResourceProvider CreateLocalResourceProvider (string virtualPath) { throw new NotSupportedException(String.Format (Thread.CurrentThread.CurrentUICulture, Properties.Resources.Provider_LocalResourcesNotSupported, "ExternalResourceProviderFactory")); } }
CreateGlobalResourceProvider() 负责通过提供的类密钥实例化 GlobalExternalResourceProvider 类型。请记住,此提供程序的类密钥必须包括程序集名称和资源类型。由于我们不在外部程序集中存储本地资源,因此 CreateLocalResourceProvider() 会引发 NotSupportedException。如果在页面上使用本地化表达式,实际上会导致解析异常。因此,如果想继续支持本地资源,这可能不是挂接 ExternalResourceProvider 的理想方案。随后,我将说明如何通过自定义的本地化表达式避免此问题。
要将 ExternalResourceProviderFactory 与现有表达式和本地化 API 挂接,我们可回到 Web.config 的<globalization>部分。
<globalization uiCulture="auto" culture="auto" resourceProviderFactoryType="CustomResourceProviders.ExternalResourceProviderFactory, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1" />
现在,默认提供程序模型被外部资源提供程序模型替换。让我们了解一下提供程序工作原理。
GlobalExternalResourceProvider
GlobalExternalResourceProvider 实现 IResourceProvider。此提供程序与 GlobalResXResourceProvider 非常类似,仅有以下一处例外:此提供程序从预先存在的附属程序集检索全局资源,并且要求知道存储资源的特定程序集名称。
GlobalExternalResourceProvider 的构造函数接收用管道符号 ("|") 隔开的程序集名称和资源类型。此信息解析如下。
public GlobalExternalResourceProvider(string classKey) { if (classKey.IndexOf('|') > 0) { string[] textArray = classKey.Split('|'); this.m_assemblyName = textArray[0]; this.m_classKey = textArray[1]; } else throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Provider_InvalidConstructor, classKey)); }
如果传递到构造函数的 classKey 参数格式无效,则将引发 ArgumentException。这将导致页面分析程序报告显式表达式错误。针对本地化 API 直接编写的代码将在运行时失败。
针对每个独特的程序集和资源类型组合,都创建并缓存了一个提供程序实例。在页面解析(用于验证)或运行时期间请求资源时,将调用 GetObject(),如下所示。
public object GetObject(string resourceKey, System.Globalization.CultureInfo culture) { this.EnsureResourceManager(); if (culture == null) { culture = CultureInfo.CurrentUICulture; } return this.m_resourceManager.GetObject(resourceKey, culture); }
在内部,GlobalExternalResourceProvider 依赖 ResourceManager 类型的现有功能来检索资源及处理资源回退。技巧在于为正确的程序集创建 ResourceManager。首次调用 EnsureResourceManager() 时,将加载资源程序集并在此程序集内为特定类型创建一个 ResourceManager 实例。如果指定不包含资源类型的程序集,则将出现异常。加载程序集并创建 ResourceManager 的代码显示如下。
Assembly asm = Assembly.Load(this.m_assemblyName); ResourceManager rm = new ResourceManager(String.Format(CultureInfo.InvariantCulture, "{0}.{1}", this.m_assemblyName, this.m_classKey), asm); this.m_resourceManager = rm;
使用 ExternalResourceProvider,可以从部署到 Web 应用程序 \bin 目录的任何程序集或“全局程序集缓存”(GAC) 检索资源。
由于本地资源不受支持,提供程序将针对 ResourceReader 属性返回 NotSupportedException;因此,隐式本地化表达式将得不到解析。
支持自定义本地化表达式
在以下情况下配置自定义提供程序非常合适,即所有资源均将存储在一个备用位置,并且您不打算利用分别位于 App_LocalResources 和 App_GlobalResources 中的资源。如果想支持本地和全局资源(默认提供程序)的标准实现,同时可选择从其他资源(自定义提供程序)提取某些资源,应该怎么办?可通过实现以自定义资源提供程序为目标的自定义表达式来满足上述要求。
ResourceExpressionBuilder 工作原理
在编译前,表达式由与页面分析步骤交互的表达式生成器来处理。表达式包括由 <%$ %> 分隔的任意内容 — 包括应用程序设置、连接字符串和本地化表达式。这类表达式的语法如下所示。
<%$ [prefix]: [declaration] %>
如您所知,本地化表达式使用前缀“Resources”。页面分析程序使用 ResourceExpressionBuilder 类型处理这些表达式。原因是 ResourceExpressionBuilder 已被映射到前缀“Resources”作为 <expressionBuilders> 的运行时默认配置。
<expressionBuilders> <add expressionPrefix="Resources" type="System.Web.Compilation.ResourceExpressionBuilder, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/> </expressionBuilders>
对于编译页面,其工作原理如下所示:
• |
分析页面时,将调用表达式生成器的 ParseExpression 操作以验证表达式语法。如果表达式无效(例如,无法找到特定资源),则将产生分析错误。 |
• |
如果分析成功,则将调用表达式生成器的 GetCodeExpression 操作以请求为表达式生成代码。表达式生成器即在此为页面初始化生成代码。代码将注入页面的已编译 IL。 |
禁用页面编译时,表达式以一种略有不同的方式进行判断。您可以禁用单个页面的页面编译。
<%@ Page Language="C#" CompilationMode="Never" %>
或者您可以在 Web.config 文件中禁用所有页面的编译。
<pages compilationMode="Never" />
在此情况下,请求页面时,将对该页面进行分析;分析期间将检查表达式生成器的 SupportsEvaluate 属性,以查看页面是否可以不编译即进行处理。不生成页面代码。
在运行时,会再次验证 SupportsEvaluate,随后调用 EvaluateExpression,以检索每个本地化表达式的值。
ResourceExpressionBuilder 从 ExpressionBuilder 派生而来。ExpressionBuilder 是公开抽象和虚拟方法的公共基类,它由 ResourceExpressionBuilder 实现,以支持页面分析、代码生成以及表达式判断。因此,要支持自定义本地化表达式,可扩展 ExpressionBuilder 并提供自己的实现。
扩展 ExpressionBuilder
要支持自定义本地化表达式,您需要自定义表达式生成器实现。与 ResourceExpressionBuilder 一样,您可以扩展 ExpressionBuilder 并为未编译页面提供页面分析、代码生成以及表达式判断的自定义实现。
首先,让我们回顾一下此示例中自定义表达式生成器的用途以及有关其实现的预期语法。目标是完整保留 <%$ Resources %> 的默认实现,同时支持从外部程序集得到的资源。要实现上述目标,我们将创建新的表达式来处理此问题,而不是完全替换资源提供程序。这意味着我们需要新的表达式前缀、自定义 ExpressionBuilder 和将此新前缀与自定义 ExpressionBuilder 相关联的方法。
在此例中,新前缀为“ExternalResource”。此新表达式的所需语法如下所示。
<%$ ExternalResource: [assemblyName]|[resourceType], [resourceKey] %>
此表达式将使用先前介绍的同一 GlobalExternalResourceProvider 从特定程序集提取资源。为支持这一新表达式,我们将创建一个自定义类型 ExternalResourceExpressionBuilder。表 2 总结了由每个替换的 ExpressionBuilder 方法所提供的功能。
表 2 总结了由每个替换方法所提供的功能 | |
方法 | 说明 |
EvaluateExpression |
在未编译页面中返回 ExternalResource 表达式的资源值。 |
GetCodeExpression |
返回为 ExternalResource 表达式生成的代码。此代码将调用自定义资源提供程序 GlobalExternalResourceProvider。 |
ParseExpression |
通过尝试访问表达式资源来验证 ExternalResource 表达式。如果无法找到资源,页面分析将失败。 |
SupportsEvaluate |
属性 指示是否支持未编译页面判断。在此实现中,将返回 true。 |
使用 ExternalResourceExpressionBuilder,可声明如下所示的自定义本地化表达式。
<asp:Label ID="labExternalResource" runat="server" Text="<%$ ExternalResources:CommonResources|CommonTerms, Hello %>" meta:localize="false" ></asp:Label>
请记住,表达式在设计时并且在编译前进行分析。在页面分析期间会调用 ParseExpression,以验证资源表达式是否准确以及请求的资源是否实际存在。以下代码说明了此实现。
public override object ParseExpression(string expression, Type propertyType, ExpressionBuilderContext context) { if (string.IsNullOrEmpty(expression)) { throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture,Properties.Resources.Expression_TooFewParameters, expression)); } ExternalResourceExpressionFields fields = null; string classKey = null; string resourceKey = null; string[] expParams = expression.Split(new char[] { ',' }); if (expParams.Length > 2) { throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Expression_TooManyParameters, expression)); } if (expParams.Length == 1) { throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Expression_TooFewParameters, expression)); } else { classKey = expParams[0].Trim(); resourceKey = expParams[1].Trim(); } fields = new ExternalResourceExpressionFields(classKey, resourceKey); ExternalResourceExpressionBuilder.EnsureResourceProviderFactory(); IResourceProvider rp = ExternalResourceExpressionBuilder. s_resourceProviderFactory.CreateGlobalResourceProvider(fields.ClassKey); object res = rp.GetObject(fields.ResourceKey, CultureInfo.InvariantCulture); if (res == null) { throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_ResourceNotFound, fields.ResourceKey)); } return fields; }
多数代码集中在验证表达式上,但核心目的是创建 GlobalExternalResourceProvider 以及调用 GetObject() 来检索资源。
编译页面时,将在页面分析后生成代码。此时,将调用表达式生成器的 GetCodeExpression 实现。此操作将返回在运行时检索资源值所需的代码,如下所示。
public override System.CodeDom.CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context) { ExternalResourceExpressionFields fields = parsedData as ExternalResourceExpressionFields; CodeMethodInvokeExpression exp = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(typeof(ExternalResourceExpressionBuilder)), "GetGlobalResourceObject", new CodePrimitiveExpression(fields.ClassKey), new CodePrimitiveExpression(fields.ResourceKey)); return exp; }
GetCodeExpression 的输出内容将导致生成的代码类似于如下所示的粗黑体代码。
labExternalResource.Text = ExternalResourceExpressionBuilder.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello") as string;
您会注意到,生成的代码依赖由 ExternalResourceExpressionBuilder 实现的静态方法。GetGlobalResourceObject 是一种辅助方法,可实例化 GlobalExternalResourceProvider 并且检索资源条目。对于已编译页面,此代码在运行时从外部资源检索值。
对于未编译页面,在运行时通过调用 EvaluateExpression 来对表达式进行判断。ExternalResourceExpressionBuilder 实现 EvaluateExpression 的替换,其再次使用 GlobalExternalResourceProvider 检索适合的资源。
public override object EvaluateExpression(object target, BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context) { ExternalResourceExpressionFields fields = parsedData as ExternalResourceExpressionFields; ExternalResourceExpressionBuilder.EnsureResourceProviderFactory(); IResourceProvider provider = ExternalResourceExpressionBuilder. s_resourceProviderFactory.CreateGlobalResourceProvider(fields.ClassKey); return provider.GetObject(fields.ResourceKey, null); }
配置自定义表达式生成器后,您可以随意加入声明语句,以便从外部程序集检索资源,同时默认本地化表达式仍用于从 App_LocalResources 或 App_GlobalResources 检索值。
ExpressionBuilder 配置
要配置自定义表达式构建器,您需要将其添加到 Web.config 中的 <expressionBuilders> 部分。在本例中,我们将“ExternalResources”前缀的 ExternalResourceExpressionBuilder 与此配置关联。
<expressionBuilders> <add expressionPrefix="ExternalResources" type="CustomResourceProviders.ExternalResourceExpressionBuilder, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1"/> </expressionBuilders>
现在,将根据先前部分介绍的 ExternalResourceExpressionBuilder 实现分析或判断所有使用“ExternalResources”前缀的资源表达式。
访问本地、全局和外部资源
列表 5 说明了使用从默认和自定义来源提取资源的全部三种本地化表达式(隐式、显式和自定义显式)的应用程序。
列表 5. 单一页面中的隐式、显式和自定义显式本地化表达式
<asp:Label ID="labHelloLocal" runat="server" Text="HelloDefault" meta:resourcekey="labHelloLocalResource1" ></asp:Label> <asp:Label ID="Label1" runat="server" Text="<%$ Resources:labHelloLocalResource1.Text %>" ></asp:Label> <asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>" ></asp:Label> <asp:Label ID="labExternalResource" runat="server" Text="<%$ ExternalResources:CommonResources|CommonTerms, Hello %>" meta:localize="false" ></asp:Label>
使用外部资源的自定义本地化表达式,而不是配置替换资源提供程序,这样使您可以使用每个页面的本地资源和通过网站编译的全局资源,同时仍然可以灵活地从外部程序集(或其他来源)选择资源。通过前面介绍的静态辅助方法 GetGlobalResourceObject(),您还可以使用 ExternalResourceExpressionBuilder 直接从代码访问外部资源。
string s = ExternalResourceExpressionBuilder.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello") as string;
使用此技术,您无需用自己的提供程序替换默认资源提供程序。而是要依赖从自定义本地化表达式生成的代码根据需要从外部程序集提取资源。
结束语
在本文中,您了解了如何创建自定义资源提供程序模型,以便从数据库或外部资源程序集访问资源。还了解了如何创建自定义本地化表达式将备用资源存储与默认提供程序模型合并。通过使用 ASP.NET 2.0 的扩展功能,您会拥有一些用于资源分配和检索的非常可行的替代方法。最有价值的部分是如何使用声明的本地化表达式方便地挂接到普通 ASP.NET 2.0 编程模型。
本系列文章中的下一篇文章将探讨关于此描述内容的另一部分:如何与构建资源表达式以及在适合的存储位置生成资源的设计时体验挂接。