Declarative ASP.NET 全球化






4.91/5 (41投票s)
一篇关于如何通过特性和反射实现 ASP.NET 页面的全球化支持的文章
引言
即使我作为一个老派的 C++ 程序员,最初认为反射是一种略显邪恶的技术,但我必须承认,利用它确实可以做一些很酷的事情。反射与特性结合是一个强大的工具,它允许以富有创意的方式实现各种功能。在这篇文章中,我将展示如何通过特性和反射以声明式的方式创建多语言的 ASP.NET 网页。
为了使本文篇幅适中,我将专注于本地化网页上简单控件的文本内容。本地化内容和更复杂的控件值得单独写一篇文章。实际上,本地化一个简单控件归结为从资源中检索特定语言的字符串,并将其赋给控件的一个属性。我们将把这段代码实现到一个小框架中,这样网页开发者就不需要自己编写了。
我们的全球化支持框架将有三个目标。首先,我们将尽量为网页开发者提供便利。这意味着,为了让服务器端控件实现本地化,在简单的情况下,开发者除了声明一个特性之外,不需要编写任何代码。其次,我们将以一种不侵入的方式实现框架。一些全球化解决方案基于继承,要求页面或控件类继承一个处理全球化任务的类。使用特性则侵入性较小,因为网页可能已经有了通用的基类,而继承另一个类可能不可行。最后,我们将使全球化框架具有可扩展性,允许开发者处理更复杂的本地化场景。
创建一个全球化意识的网页
我们将首先看看开发者如何使用我们的框架创建全球化意识的网页。为了本地化网页上的控件,开发者需要用 Localize
特性标记它们,并在资源中提供相应的特定语言字符串。在最简单的情况下,这就是开发者需要编写的所有代码。下面的代码片段展示了代码后置页面类中的代码样子。
[Localize(Mode=LocalizeMode.Fields,
ResourceBaseName="MyWebApp.Strings")]
public class WebForm1 : System.Web.UI.Page
{
[Localize()]
protected System.Web.UI.WebControls.Label lblCopyright;
// etc...
}
这里值得关注的是附加到页面类和 lblCopyright
标签控件上的 Localize
特性。此特性将在运行时导致控件中的文本被替换为特定语言的字符串。从技术上讲,附加到 WebForm1
页面类上的 Localize
特性并非绝对必要;我们也可以通过附加到各个控件上的 Localize
特性提供所有信息。我决定实现全球化支持,允许在网页类上附加一种根特性。这样,网页开发者就可以在根特性中包含通用的全球化信息,而无需在页面上每个控件的特性中重复。这是有意义的,因为一个网页的大部分资源很可能驻留在同一个源中,例如,一个资源程序集。根特性还允许实现快速决定页面是否需要全球化。
除了将特性附加到控件上,还需要进行一些配置。我们需要对 web 应用程序配置文件进行两项修改。首先,需要告知 ASP.NET GlobalizationMod
HttpModule,它负责大部分全球化工作。其次,需要将网站的默认语言传达给全球化模块。这两项修改都将在 web.config 文件中完成,如下所示:
<system.web>
<httpModules>
<add name="GlobalizationModule"
type="GlobalizationModule.GlobalizationMod, GlobalizationModule" />
</httpModules>
</system.web>
<appSettings>
<add key="DefaultLanguage" value="en-US"/>
</appSettings>
挂入 HTTP 管道
既然我们已经从网页程序员的视角看到了代码的样子,让我们来看看实现。我们需要在页面构建和初始化之后,但在它将 HTML 渲染到输出流之前,替换控件中的文本为特定语言的字符串。为了做到这一点,我们需要挂入 ASP.NET HTTP 管道。下图描绘了管道。
当一个 web 请求从网络传来时,IIS 会处理它,并将其传递给 aspnet_isapi.dll,该 DLL 已注册以处理 .ASPX 页面的请求。aspnet_isapi.dll 然后将请求传递给相应的 HttpApplication
实例。HttpApplication
会处理请求,并将其交给一个或多个 HttpModule
对象。HttpModule
执行诸如身份验证和缓存之类的任务,它们可以在 HttpHandler
(即网页类)处理请求之前、期间和之后在管道中发挥作用。通过实现 HttpModule,我们可以在管道中的正确位置执行全球化逻辑。
实现 HttpModule
主要就是创建一个实现 IHttpModule
接口的类。它有一个我们感兴趣的方法 void Init(HttpApplication)
,我们在其中准备处理 HttpApplication
的 PreRequestHandlerExecute
事件。
public class GlobalizationMod : IHttpModule
{
public void Init(HttpApplication app)
{
app.PreRequestHandlerExecute += new EventHandler(this.OnPreRequest);
}
// etc…
}
这里情况有点棘手。在我们的 OnPreRequest
处理器中,我们可以轻松地获取到将要进行全球化处理的 web 页面的实例。但事实证明,此时 web 页面的子控件尚未实例化——页面的数据成员为 null。为了在子控件创建后、渲染之前访问它们,我们需要挂入 web 页面的 PreRender
事件。因此,OnPreRequest
只是简单地向 web 页面的 PreRender
事件添加一个处理程序。
public void OnPreRequest(object sender, EventArgs eventArgs)
{
// Get the IHttpHandler from the HttpApplication
HttpContext ctx = ((HttpApplication)sender).Context;
IHttpHandler handler = ctx.Handler;
// Handle the PreRender event of the page
((System.Web.UI.Page)handler).PreRender += new EventHandler(
this.OnPreRender);
}
既然我们已经成功地挂入了 HTTP 管道的正确位置,我们终于可以开始在 OnPreRender
中编写全球化支持实现的核心代码了。
处理 Localize 特性
真正的 C++ 工作从GlobalizationMod
HttpModule
的 OnPreRender
方法开始。代码首先检查当前语言是否为网站的默认语言,如果是,则退出,因为没有什么可做的。否则,代码继续查看页面类是否附加了 Localize
特性。如果是,我们将调用一个内部辅助函数 LocalizeObject
,并将页面和特性作为参数传递。public void OnPreRender(object sender, EventArgs eventArgs)
{
// If current culture is the same as the web site's
// default language, then we do nothing
CultureInfo currentCulture =
System.Threading.Thread.CurrentThread.CurrentUICulture;
if (ConfigurationSettings.AppSettings["DefaultLanguage"] ==
currentCulture.Name)
return;
System.Web.UI.Page page = (System.Web.UI.Page)sender;
// Localize the page if it has the Localize attribute
object[] typeAttrs = page.GetType().GetCustomAttributes(
typeof(LocalizeAttribute), true);
if (typeAttrs != null && typeAttrs.Length > 0)
{
LocalizeObject(null, sender, (LocalizeAttribute)typeAttrs[0]);
}
}
LocalizeObject
是框架中最复杂的函数。它首先查看与目标对象关联的特性。如果特性的本地化模式为 LocalizeMode.Fields
,则表示我们应该本地化对象的字段而不是对象本身。在这种情况下,代码通过反射检索对象的字段,并递归地为每个带有 Localize
特性的子对象调用 LocalizeObject
。LocalizeObject
的代码如下所示:
public class GlobalizationMod : IHttpModule
{
// ...
protected void LocalizeObject(FieldInfo fieldInfo, object target,
LocalizeAttribute attr)
{
if (attr.Mode == LocalizeAttribute.LocalizeMode.Fields)
{
//
// Localize child objects
//
FieldInfo[] fields = null;
Type targetType = null;
if (target is System.Web.UI.Page)
{ // Remember the web page class
targetPage_ = (System.Web.UI.Page)target;
// Remember root attribute
rootAttribute_ = attr;
targetType = target.GetType().BaseType;
}
else
{
targetType = target.GetType();
}
//
// Localize fields that have the Localize attribute
//
fields = targetType.GetFields(BindingFlags.Instance|
BindingFlags.NonPublic|BindingFlags.Public);
foreach (FieldInfo f in fields)
{
// Get the child instance
object child = f.GetValue(target);
if (child != null)
{
// Localize this object if it has the Localize attribute
object[] typeAttrs = f.GetCustomAttributes(
typeof(LocalizeAttribute), true);
if (typeAttrs != null && typeAttrs.Length > 0)
{
LocalizeObject(f, child, (LocalizeAttribute)
typeAttrs[0]);
}
}
}
}
else
{
// If this attribute has no resource name specified,
// use the root attribute's resource name as base resource name.
if (attr.ResourceBaseName == null)
attr.ResourceBaseName = rootAttribute_.ResourceBaseName;
// If this attribute has no resource name specified,
// use the target object's name as resource name.
if (attr.ResourceName == null)
attr.ResourceName = fieldInfo.Name;
// The actual localization of the target object is performed
// in a virtual method of the attribute. This way the developer
// can implement her own localization logic by deriving a class
// from LocalizeAttribute and overriding the Localize method.
attr.LocalizeObject(target, targetPage_);
}
}
}
对于本地化模式不是 LocalizeMode.Fields
的那些对象,代码将继续本地化对象本身。本地化的实际逻辑位于特性类的 LocalizeObject
方法中。将实际逻辑放在特性类的虚拟方法中,使得开发者可以扩展全球化框架。
在通过从卫星程序集中加载特定语言的字符串并将其赋给目标对象属性来完成本地化的简单情况下,LocalizeAttribute.LocalizeObject
中的实现就足够了。
public class LocalizeAttribute : Attribute
{
// ...
public virtual void LocalizeObject(object target, System.Web.UI.Page page)
{
// User's page class is superclass of the ASP.NET page class
Type userPageClass = page.GetType().BaseType;
// The user's assembly is the one that holds his/her page class
Assembly targetAssembly = userPageClass.Assembly;
CultureInfo culture =
System.Threading.Thread.CurrentThread.CurrentUICulture;
ResourceManager resMan = new ResourceManager(ResourceBaseName,
targetAssembly);
// There are a number of ways we could handle the
// case when a resource
// is not found - we could e.g., simply allow the exception
// to pass and
// let the global OnError handler handle it. Instead, we'll show
string s = string.Format(
"<font color=\"red\">NO RESOURCE FOUND FOR CULTURE: {0}</font>",
culture.Name);
try
{
s = resMan.GetString(ResourceName, culture);
}
catch (MissingManifestResourceException)
{}
// Invoke the target's Action property. Most of the time this
// means we'll set the target's Text property.
target.GetType().InvokeMember(
this.Action,
BindingFlags.SetProperty,
null,
target,
new object[]{ s });
}
}
上面的代码从卫星程序集中加载当前语言的字符串。目标对象的名称(例如 lblCopyright
)用作资源的名称。最后,代码调用 InvokeMember
并使用 BindingFlags.SetProperty
标志将字符串赋给目标对象的属性。默认情况下,特性的 Action
属性的值为 'Text',因此默认情况下字符串被赋给目标对象的 Text
属性。换句话说,对于 lblCopyright
控件,代码实际上执行了以下操作:
lblCopyright.Text = resourceManager.GetString("lblCopyright", CurrentCulture);
在控件的本地化需要更复杂逻辑的情况下,开发者可以派生一个新类自 LocalizeAttribute
,并将新特性附加到控件上。为了实现特殊的本地化逻辑,开发者会重写 LocalizeObject
方法,当需要本地化控件时,框架将调用她的代码。
缺少资源的问题通过将特殊的错误字符串放入目标控件来处理。这在开发期间还可以,但在实际发布 web 应用程序之前,您会希望有一些不太容易出错的方案。确保没有缺少资源会遗漏到发布版本中的一个好方法是进行单元测试,该测试会访问所有支持语言的所有页面,并断言在任何页面上都没有出现错误字符串。由于我们都使用单元测试框架,编写此类测试易如反掌,这应该不难吧?
摘要
本文介绍的框架允许开发者以声明式的方式创建多语言网站。控件通过将其附加特性并提供卫星程序集中的相应特定语言字符串资源来本地化。
示例项目包含一个 web 应用程序的源代码,该应用程序的所有字符串都本地化为四种语言(若有任何语言不正确,敬请谅解)。此外,它还展示了如何本地化 ImageButton
,以及框架如何扩展以本地化更复杂的控件,如 DataList
。希望您觉得这些想法有用。
历史
- 2004 年 4 月 16 日 - 更新了源代码