|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionEven 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 pageWe'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(Mode=LocalizeMode.Fields,
ResourceBaseName="MyWebApp.Strings")]
public class WebForm1 : System.Web.UI.Page
{
[Localize()]
protected System.Web.UI.WebControls.Label lblCopyright;
// etc...
}
The points of interest here are the 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 <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 pipelineNow 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 Implementing an public class GlobalizationMod : IHttpModule
{
public void Init(HttpApplication app)
{
app.PreRequestHandlerExecute += new EventHandler(this.OnPreRequest);
}
// etc…
}
This is where things get a bit tricky. In our 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);
}
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 Processing Localize attributesThe real work begins in theOnPreRender 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)
{
// 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]);
}
}
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_);
}
}
}
For those objects whose localization mode is not 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 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 });
}
}
The code above loads the string for the current language from a satellite assembly. The name of the target object (e.g.,
In cases where the localization of a control requires more complex logic, the developer can derive a new class from 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? SummaryThe 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 History
| ||||||||||||||||||||