<!--------------------------------------------------------------------------->
<!-- INTRODUCTION
The Code Project article submission template (HTML version)
Using this template will help us post your article sooner. To use, just
follow the 3 easy steps below:
1. Fill in the article description details
2. Add links to your images and downloads
3. Include the main article text
That's all there is to it! All formatting will be done by our submission
scripts and style sheets.
-->
<!--------------------------------------------------------------------------->
<!-- IGNORE THIS SECTION --><html><head>
<title>The Code Project</title>
<STYLE> BODY, P, TD { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt }
H2,H3,H4,H5 { color: #ff9900; font-weight: bold; }
H2 { font-size: 13pt; }
H3 { font-size: 12pt; }
H4 { font-size: 10pt; color: black; }
PRE { BACKGROUND-COLOR: #FBEDBB; FONT-FAMILY: "Courier New", Courier, mono; WHITE-SPACE: pre; }
CODE { COLOR: #990000; FONT-FAMILY: "Courier New", Courier, mono; }
</STYLE>
<link href="http://www.codeproject.com/styles/global.css" type="text/css" rel="stylesheet"></head>
<body bgColor="#ffffff" color="#000000">
<!--------------------------------------------------------------------------->
<!------------------------------- STEP 1 --------------------------->
<!-- Fill in the details (CodeProject will reformat this section for you) --><pre>Title: Declarative ASP.NET globalization
Author: Sami Vaaraniemi
Email: samiva@jippii.fi
Environment: .NET, C#, ASP.NET
Keywords: ASP.NET, globalization, attribute, reflection, resource, satellite assembly
Level: Intermediate
Description: An article on how to implement globalization support for ASP.NET pages through
attributes and reflection
Section ASP.NET
SubSection General
</pre>
<!------------------------------- STEP 2 --------------------------->
<!-- Include download and sample image information. -->
<ul>
<li>
<A href="http://www.icustomsoftware.net/MyWebApp/webform1.aspx">View live application</A>
<li class="download">
<A href="DeclarativeGlobalization_src.zip">Download source - 39 Kb</A>
</li>
</ul>
<!------------------------------- STEP 3 --------------------------->
<!-- Add the article text. Please use simple formatting (<h2>, <p> etc) -->
<h2>
Introduction
</h2>
<p>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.
</p>
<p>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.
</p>
<P>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.
</P>
<h2>Making a globalization-aware web page</h2>
<p>
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 <code>Localize</code> 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:
</p>
<pre lang="cs">
[Localize(Mode=LocalizeMode.Fields,
ResourceBaseName="MyWebApp.Strings")]
public class WebForm1 : System.Web.UI.Page
{
[Localize()]
protected System.Web.UI.WebControls.Label lblCopyright;
// etc...
} </pre>
<p>The points of interest here are the <code>Localize</code> attributes attached to
the page class and to the <code>lblCopyright</code> label control. This
attribute will cause the text in the control to be replaced with a language
specific string at runtime. Technically, the <code>Localize</code> attribute
attached to the <code>WebForm1</code> page class is not absolutely necessary;
we could just as well have provided all the information through <code>Localize</code>
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.
</p>
<P>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 <code>GlobalizationMod</code> 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:</P>
<pre><system.web>
<httpModules>
<add name="GlobalizationModule" type="GlobalizationModule.GlobalizationMod, GlobalizationModule" />
</httpModules>
</system.web>
<appSettings>
<add key="DefaultLanguage" value="en-US"/>
</appSettings></pre>
<h2>Hooking into the HTTP pipeline</h2>
<p>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.
</p>
<p><IMG width="584" height="117" alt="ASP.NET HTTP pipeline" src="httppipeline.gif"></p>
<p>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. HttpModules 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.
</p>
<p>Implementing an HttpModule boils down to creating a class that implements the <code>IHttpModule</code>
interface. It has one method of interest, <code>void Init(HttpApplication)</code>,
in which we prepare to handle the <code>PreRequestHandlerExecute</code> event
of the HttpApplication:
</p>
<pre lang="cs">public class GlobalizationMod : IHttpModule
{
public void Init(HttpApplication app)
{
app.PreRequestHandlerExecute += new EventHandler(this.OnPreRequest);
}
// etc�
} </pre>
<P></P>
<p>This is where things get a bit tricky. In our <code>OnPreRequest</code> 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 <code>PreRender</code> event of the web page
class. Therefore, <code>OnPreRequest</code> simply adds a handler to the <code>PreRender</code>
event of the web page:
</p>
<pre lang="cs">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);
} </pre>
<p>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 <code>OnPreRender</code>.
</p>
<h2>Processing Localize attributes</h2>
The real work begins in the <code>OnPreRender</code> method of the <code>GlobalizationMod</code>
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 <code>Localize</code> attribute
attached to it. If so, we call an internal helper function <code>LocalizeObject</code>
with the page and the attribute as parameters.
<pre lang="cs">
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]);
}
}</pre>
<p><code>LocalizeObject</code> 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 <code>LocalizeMode.Fields</code>, 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 <code>LocalizeObject</code> recursively for each
child object that has the <code>Localize</code> attribute attached to it. The
code for <code>LocalizeObject</code> is shown below:
</p>
<pre lang="cs">
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_);
}
}
}</pre>
<p>For those objects whose localization mode is not <code>LocalizeMode.Fields</code>,
the code proceeds to localize the object itself. The actual logic of the
localization is in the <CODE>LocalizeObject</CODE> 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.
</p>
<P>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 <code>LocalizeAttribute.LocalizeObject</code>
is enough:</P>
<pre lang="cs">
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 });
}
}</pre>
<P>The code above loads the string for the current language from a satellite
assembly. The name of the target object (e.g., <code>lblCopyright</code>) is
used as the name of the resource. Finally, the code calls <code>InvokeMember</code>
with the <code>BindingFlags.SetProperty</code> flag to assign the string to a
property of the target object. By default, the value of the attribute's <code>Action</code>
property is 'Text', and therefore by default the string is assigned to the <code>Text</code>
property of the target object. In other words, in the case of the <code>lblCopyright</code>
control, the code does effectively this:</P>
<P> <code>lblCopyright.Text =
resourceManager.GetString("lblCopyright", CurrentCulture);</code></P>
<P>
In cases where the localization of a control requires more complex logic, the
developer can derive a new class from <code>LocalizeAttribute</code> and attach
the new attribute to the control. To implement the special localization logic,
the developer overrides the <code>LocalizeObject</code> method and the
framework will call her code when it is time to localize the control.</P>
<P>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?</P>
<h2>Summary</h2>
<P>
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.</P>
<P>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.
<!------------------------------- That's it! ---------------------------></P>
</body>
</html>