Introduction
Even as an old-school C++ programmer who initially regarded reflection as mildly evil I must admit that you can actually do pretty cool things with it. Reflection together with attributes is a powerful tool that allows for creative ways of implementing things. In this article, I will show how to create multi-lingual ASP.NET web pages declaratively through attributes and reflection.
To keep the size of this article reasonable I'm going to concentrate on localizing text contents of simple controls on a web page. Localizing content and more complex controls warrant an article of their own. In practice the localization of a simple control boils down to retrieving a language specific string from a resource and assigning it to a property of the control. We'll implement this code in a small framework so the web page developer doesn't have to write it.
The goals for our globalization support framework will be three-fold. First, we’ll want to make things as convenient as possible for the web page developer. This means that in order to get a server side control localized, in the simple case the developer will not have to write code other than declaring an attribute. Second, we’ll want to implement the framework in non-intrusive manner. Some globalization solutions are based on inheritance requiring the page or the control class to derive from a class that handles the localization tasks. Using attributes is less intrusive as there might already be a common base class for the web pages and inheriting from another class might not be feasible. Finally, we’ll want to make the globalization framework extensible to allow the developer to handle more complex localization situations.
Making a globalization-aware web page
We'll begin by taking a look at how a developer creates a globalization-aware web page using our framework. To localize controls on a web page, the developer tags them with Localize
attributes and provides the appropriate language specific strings in a resource. In the simplest case, this is all the code the developer has to write. The following code snippet shows what the code looks like in the code behind page class:
[Localize(Mode=LocalizeMode.Fields,
ResourceBaseName="MyWebApp.Strings")]
public class WebForm1 : System.Web.UI.Page
{
[Localize()]
protected System.Web.UI.WebControls.Label lblCopyright;
}
The points of interest here are the Localize
attributes attached to the page class and to the lblCopyright
label control. This attribute will cause the text in the control to be replaced with a language specific string at runtime. Technically, the Localize
attribute attached to the WebForm1
page class is not absolutely necessary; we could just as well have provided all the information through Localize
attributes attached to the individual controls. I decided to implement the globalization support to allow a kind of a root attribute to be attached to the web page class. This way the web page developer can have common localization information in the root attribute and does not need to repeat it in every attribute attached to controls on one page. This makes sense as most of the resources for one web page are likely to reside in one source, e.g., a resource assembly. The root attribute also allows the implementation to quickly decide whether a page needs localization or not.
In addition to attaching attributes to the controls, there is some configuration to do. We need to make two additions to the web application configuration file. First, ASP.NET needs to be told about the GlobalizationMod
HttpModule that does most of the leg work in the globalization. Second, the web site's default language needs to be communicated to the globalization module. Both additions go to the web.config file and are shown below:
<system.web>
<httpModules>
<add name="GlobalizationModule"
type="GlobalizationModule.GlobalizationMod, GlobalizationModule" />
</httpModules>
</system.web>
<appSettings>
<add key="DefaultLanguage" value="en-US"/>
</appSettings>
Hooking into the HTTP pipeline
Now that we’ve seen what the code looks like from the web page programmer’s point of view, let’s take a look at the implementation. We need to replace the text in the controls with language specific strings after the page is constructed and initialized but before it renders the HTML into the output stream. In order to do this, we need to hook into the ASP.NET HTTP pipeline. The figure below depicts the pipeline.
When a web request comes from the network, IIS handles it and passes it over to aspnet_isapi.dll which is registered to handle requests for .ASPX pages. aspnet_isapi.dll then passes the request to the appropriate HttpApplication
instance. The HttpApplication
massages the request and hands it over to one or more HttpModule
objects. HttpModule
s perform tasks such as authentication and caching, and they get to act in the pipeline before, during and after an HttpHandler
(i.e., the web page class) processes the request. By implementing an HttpModule we can do the localization logic at the right place in the pipeline.
Implementing an HttpModule
boils down to creating a class that implements the IHttpModule
interface. It has one method of interest, void Init(HttpApplication)
, in which we prepare to handle the PreRequestHandlerExecute
event of the HttpApplication
:
public class GlobalizationMod : IHttpModule
{
public void Init(HttpApplication app)
{
app.PreRequestHandlerExecute += new EventHandler(this.OnPreRequest);
}
}
This is where things get a bit tricky. In our OnPreRequest
handler, we can easily get our hands on the web page instance we’re about to localize. But it turns out that the child controls of the web page class have not been instantiated yet at this point - the data members of the page are null. In order get to the child controls after they are created but before they render themselves, we need to hook the PreRender
event of the web page class. Therefore, OnPreRequest
simply adds a handler to the PreRender
event of the web page:
public void OnPreRequest(object sender, EventArgs eventArgs)
{
HttpContext ctx = ((HttpApplication)sender).Context;
IHttpHandler handler = ctx.Handler;
((System.Web.UI.Page)handler).PreRender += new EventHandler(
this.OnPreRender);
}
Now that we've successfully hooked into the proper place in the HTTP pipeline, we are finally ready to write the meat of the globalization support implementation in OnPreRender
.
Processing Localize attributes
The real work begins in the
OnPreRender
method of the
GlobalizationMod
HttpModule
. The code first checks if the current language is the web site’s default language, and if so, it exits as there is nothing to do. Otherwise, the code proceeds to see if the page class has the
Localize
attribute attached to it. If so, we call an internal helper function
LocalizeObject
with the page and the attribute as parameters.
public void OnPreRender(object sender, EventArgs eventArgs)
{
CultureInfo currentCulture =
System.Threading.Thread.CurrentThread.CurrentUICulture;
if (ConfigurationSettings.AppSettings["DefaultLanguage"] ==
currentCulture.Name)
return;
System.Web.UI.Page page = (System.Web.UI.Page)sender;
object[] typeAttrs = page.GetType().GetCustomAttributes(
typeof(LocalizeAttribute), true);
if (typeAttrs != null && typeAttrs.Length > 0)
{
LocalizeObject(null, sender, (LocalizeAttribute)typeAttrs[0]);
}
}
LocalizeObject
is by far the most complex function in the framework. It first looks at the attribute associated with the target object. If the attributes localize mode is LocalizeMode.Fields
, it indicates that we should localize the fields of the object rather than the object itself. In this case, the code retrieves the fields of the object through reflection and calls LocalizeObject
recursively for each child object that has the Localize
attribute attached to it. The code for LocalizeObject
is shown below:
public class GlobalizationMod : IHttpModule
{
protected void LocalizeObject(FieldInfo fieldInfo, object target,
LocalizeAttribute attr)
{
if (attr.Mode == LocalizeAttribute.LocalizeMode.Fields)
{
FieldInfo[] fields = null;
Type targetType = null;
if (target is System.Web.UI.Page)
{ targetPage_ = (System.Web.UI.Page)target;
rootAttribute_ = attr;
targetType = target.GetType().BaseType;
}
else
{
targetType = target.GetType();
}
fields = targetType.GetFields(BindingFlags.Instance|
BindingFlags.NonPublic|BindingFlags.Public);
foreach (FieldInfo f in fields)
{
object child = f.GetValue(target);
if (child != null)
{
object[] typeAttrs = f.GetCustomAttributes(
typeof(LocalizeAttribute), true);
if (typeAttrs != null && typeAttrs.Length > 0)
{
LocalizeObject(f, child, (LocalizeAttribute)
typeAttrs[0]);
}
}
}
}
else
{
if (attr.ResourceBaseName == null)
attr.ResourceBaseName = rootAttribute_.ResourceBaseName;
if (attr.ResourceName == null)
attr.ResourceName = fieldInfo.Name;
attr.LocalizeObject(target, targetPage_);
}
}
}
For those objects whose localization mode is not LocalizeMode.Fields
, the code proceeds to localize the object itself. The actual logic of the localization is in the LocalizeObject
method of the attribute class. Having the actual logic in a virtual method of the attribute class makes it possible for the developer to extend the globalization framework.
In simple cases where the localization is done by loading the language specific string from a satellite assembly and assigning the string to a property of the target object, the implementation in the LocalizeAttribute.LocalizeObject
is enough:
public class LocalizeAttribute : Attribute
{
public virtual void LocalizeObject(object target, System.Web.UI.Page page)
{
Type userPageClass = page.GetType().BaseType;
Assembly targetAssembly = userPageClass.Assembly;
CultureInfo culture =
System.Threading.Thread.CurrentThread.CurrentUICulture;
ResourceManager resMan = new ResourceManager(ResourceBaseName,
targetAssembly);
string s = string.Format(
"<font color=\"red\">NO RESOURCE FOUND FOR CULTURE: {0}</font>",
culture.Name);
try
{
s = resMan.GetString(ResourceName, culture);
}
catch (MissingManifestResourceException)
{}
target.GetType().InvokeMember(
this.Action,
BindingFlags.SetProperty,
null,
target,
new object[]{ s });
}
}
The code above loads the string for the current language from a satellite assembly. The name of the target object (e.g., lblCopyright
) is used as the name of the resource. Finally, the code calls InvokeMember
with the BindingFlags.SetProperty
flag to assign the string to a property of the target object. By default, the value of the attribute's Action
property is 'Text', and therefore by default the string is assigned to the Text
property of the target object. In other words, in the case of the lblCopyright
control, the code does effectively this:
lblCopyright.Text = resourceManager.GetString("lblCopyright", CurrentCulture);
In cases where the localization of a control requires more complex logic, the developer can derive a new class from LocalizeAttribute
and attach the new attribute to the control. To implement the special localization logic, the developer overrides the LocalizeObject
method and the framework will call her code when it is time to localize the control.
Problems with missing resources are handled by putting a special error string into the target control. This is fine during development but you'll want to have something less prone to error before you actually release your web application. A good way to ensure that no missing resources slip into a release is to have a unit test that visits all pages with all supported languages and asserts that the error string is not present on any of the pages. That should not be difficult to do since we all use unit test frameworks that make it a breeze writing such tests, don't we?
Summary
The framework presented in this article allows the developer to create multi-lingual websites declaratively. Controls are localized by attaching attributes to them and by providing the appropriate language specific string resources in satellite assemblies.
The sample project contains the source code to a web application that has all strings localized to four languages (apologies for any incorrect language). Additionally it shows how to localize an ImageButton
and also how the framework can be extended to localize a more complex control such as a DataList
. I hope you find the ideas useful.
History
- 16 April 2004 - Source code updated